camox 0.76.0 → 0.76.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/editing/createEditableBlock.js +1077 -1681
- package/dist/core/editing/fieldSlot.js +251 -0
- package/dist/core/editing/useFieldSlot.js +159 -0
- package/dist/core/layoutContext.d.ts +2 -0
- package/dist/core/layoutContext.js +27 -1
- package/dist/features/canvas/CanvasPageFrame.js +2 -2
- package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
- package/dist/features/canvas/canvasCameraController.js +595 -0
- package/dist/features/canvas/useCanvasCamera.js +141 -587
- package/dist/features/canvas/useCanvasFrame.js +1 -1
- package/dist/features/page/DerivedPageContent.js +4 -8
- package/dist/features/preview/components/AssetLightbox.js +6 -65
- package/dist/features/preview/components/CanvasPreview.js +2 -2
- package/dist/features/preview/components/Frame.js +3 -3
- package/dist/features/preview/components/ItemFieldsEditor.js +376 -460
- package/dist/features/preview/components/Overlays.js +4 -9
- package/dist/features/preview/components/PreviewPanel.js +3 -3
- package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
- package/dist/features/preview/components/SelectionEditor.js +285 -339
- package/dist/features/preview/components/ZoomableImage.js +105 -0
- package/dist/features/preview/components/contentFieldSchema.js +3 -1
- package/dist/features/preview/components/useFieldWriter.js +30 -51
- package/dist/features/preview/components/useRecordView.js +3 -3
- package/dist/features/preview/overlayMessages.js +20 -58
- package/dist/features/preview/previewStore.js +6 -123
- package/dist/features/preview/repeatableItemPath.js +47 -0
- package/dist/features/renderer/RendererApp.js +6 -6
- package/dist/features/renderer/RendererFrames.js +2 -2
- package/dist/features/renderer/rendererHost.js +1 -1
- package/dist/features/routes/pageRuntime.d.ts +2 -8
- package/dist/features/routes/pageRuntime.js +21 -105
- package/dist/features/runtime/pageApp.js +1 -1
- package/dist/features/runtime/pageRead.js +105 -0
- package/dist/features/runtime/reader.d.ts +25 -0
- package/dist/features/runtime/reader.js +142 -0
- package/dist/features/runtime/runtime.d.ts +9 -3
- package/dist/features/runtime/runtime.js +608 -776
- package/dist/features/sdkBundle/buildSdkBundle.d.ts +5 -0
- package/dist/features/sdkBundle/buildSdkBundle.js +9 -3
- package/dist/features/studio/components/DraftsSheet.js +124 -65
- package/dist/features/studio/components/ThreadMessages.js +5 -9
- package/dist/features/studio/showInCanvas.js +9 -19
- package/dist/features/studio/useEditorSelection.js +8 -19
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +4 -4
- package/dist/core/hooks/useFieldSelection.js +0 -67
- package/dist/core/hooks/useIsEditable.js +0 -21
- package/dist/core/hooks/useOverlayMessage.js +0 -70
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { serializeJsonForHtml } from "../../lib/html.js";
|
|
2
|
+
import { notFound } from "../../core/notFound.js";
|
|
2
3
|
import { createRequestContext } from "../../core/requestContext.js";
|
|
3
4
|
import { UNNAMED_DEFAULT_LOCALE, localizedPath, parseLocalePath } from "../../lib/locale-path.js";
|
|
4
|
-
import {
|
|
5
|
-
import { buildClearServerAuthCookieHeader, buildServerAuthCookieHeader, getLookToken, getServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
|
|
5
|
+
import { buildServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
|
|
6
6
|
import { getCookie, getSetCookie } from "../../lib/stored-auth-cookies.js";
|
|
7
7
|
import { ARTIFACTS_PATH_PREFIX, RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute } from "./runtimeRoutes.js";
|
|
8
8
|
import { reads } from "../../lib/studio-reads.js";
|
|
9
|
-
import {
|
|
9
|
+
import { recordMarkdownTemplate } from "../../core/recordItem.js";
|
|
10
10
|
import { PLATFORM_SCRIPT } from "../../lib/platform.js";
|
|
11
11
|
import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_SETTINGS_PATH, matchCollectionContentPath } from "../studio/routes.js";
|
|
12
12
|
import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
|
|
13
13
|
import { DefaultPageImage, renderPageImage } from "../og/pageImage.js";
|
|
14
|
-
import { discoverLayoutPages
|
|
15
|
-
import {
|
|
16
|
-
import { buildCamoxPageHead, buildCamoxPageLoaderData, buildFaviconUrl, buildPageHead, buildPageMeta,
|
|
14
|
+
import { discoverLayoutPages } from "../../core/derivedRoutes.js";
|
|
15
|
+
import { curatedLayoutContext } from "../../core/layoutContext.js";
|
|
16
|
+
import { buildCamoxPageHead, buildCamoxPageLoaderData, buildFaviconUrl, buildPageHead, buildPageMeta, createServerStudioCache, curatedMarkdownResponse, declarePage, isAuthSessionError, isNotFoundError, loadCamoxPage, parseMarkdownPath, prefersMarkdown, readPageMarkdown, renderedHref, withFrontMatter } from "../routes/pageRuntime.js";
|
|
17
17
|
import { assetPrefix } from "./assetPrefix.js";
|
|
18
18
|
import { documentGoogleFonts } from "../og/documentFonts.js";
|
|
19
19
|
import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
|
|
@@ -26,7 +26,9 @@ import { formatLlmsTxt } from "./llmsTxt.js";
|
|
|
26
26
|
import { getPageBlockTypes } from "./pageBlockTypes.js";
|
|
27
27
|
import { withPageUrls } from "./structuredData.js";
|
|
28
28
|
import { curatedPageDescription, pageRecords, placedBlockNodes, withStructuredData } from "./pageGraph.js";
|
|
29
|
+
import { mayShowCuratedPage, readPage } from "./pageRead.js";
|
|
29
30
|
import { createProjectLogoResponse, projectLogo } from "./projectLogo.js";
|
|
31
|
+
import { isPrivateRequest, lookReader, resolveReader, visitorReader } from "./reader.js";
|
|
30
32
|
import { DRAFT_SEARCH_PARAM, LOCALE_HEADER, LOOK_BLOCKS_PARAM, LOOK_BLOCK_DEFINITION_PARAM, LOOK_CONTENT_PARAM, LOOK_LAYOUT_DEFINITION_PARAM, LOOK_LOCALE_PARAM, LOOK_PATH_PARAM, LOOK_PLACEHOLDER_HEIGHTS, LOOK_PLACEHOLDER_PARAM, LOOK_SETTINGS_PARAM, UNPLACED_BLOCK_ID } from "@camox/api-contract/drafts";
|
|
31
33
|
import { QueryClient, dehydrate } from "@tanstack/react-query";
|
|
32
34
|
import { createHead, renderSSRHead } from "@unhead/react/server";
|
|
@@ -53,16 +55,19 @@ function getRuntimePathname(url, runtimeBasePath = DEFAULT_RUNTIME_BASE_PATH) {
|
|
|
53
55
|
if (!pathname.startsWith(`${basePath}/`)) return null;
|
|
54
56
|
return pathname.slice(basePath.length) || "/";
|
|
55
57
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
return draftNamedBy(request.url) ?? void 0;
|
|
59
|
-
}
|
|
58
|
+
/** What a response says when no cache may keep it. */
|
|
59
|
+
const PRIVATE_HEADERS = { "Cache-Control": "private, no-store" };
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* The headers of what a request reads as `reader`: private when it may show what only this
|
|
62
|
+
* reader sees (or `alwaysPrivate`), varying by the session's cookie, which a stale session's
|
|
63
|
+
* response clears.
|
|
63
64
|
*/
|
|
64
|
-
function
|
|
65
|
-
return
|
|
65
|
+
function readerHeaders(reader, { alwaysPrivate = false } = {}) {
|
|
66
|
+
return {
|
|
67
|
+
...(alwaysPrivate || reader.private) && PRIVATE_HEADERS,
|
|
68
|
+
Vary: "Cookie",
|
|
69
|
+
...reader.setCookie && { "Set-Cookie": reader.setCookie }
|
|
70
|
+
};
|
|
66
71
|
}
|
|
67
72
|
/** The routes whose response is the same for every visitor, unless the request is private. */
|
|
68
73
|
const PUBLIC_ROUTE_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -80,7 +85,7 @@ const PUBLIC_ROUTE_KINDS = /* @__PURE__ */ new Set([
|
|
|
80
85
|
*/
|
|
81
86
|
function isPublicRequest(request, options = {}) {
|
|
82
87
|
if (request.method !== "GET" && request.method !== "HEAD") return false;
|
|
83
|
-
if (
|
|
88
|
+
if (isPrivateRequest(request) || new URL(request.url).searchParams.has("ott")) return false;
|
|
84
89
|
const pathname = getRuntimePathname(request.url, options.runtimeBasePath);
|
|
85
90
|
return pathname !== null && PUBLIC_ROUTE_KINDS.has(matchRuntimeRoute(pathname).kind);
|
|
86
91
|
}
|
|
@@ -147,9 +152,10 @@ async function respondWithLiveWebsite(options, name, respond, { everyLocale = fa
|
|
|
147
152
|
const { apiUrl, projectUuid } = options;
|
|
148
153
|
if (!apiUrl || !projectUuid) return Response.json({ message: `Camox ${name} is not configured.` }, { status: 500 });
|
|
149
154
|
const app = await options.getCamoxApp?.();
|
|
155
|
+
const reader = visitorReader(options);
|
|
150
156
|
const readLocale = async (locale) => {
|
|
151
|
-
const { projectName, pages } = await
|
|
152
|
-
const records = createRecordLookup(options,
|
|
157
|
+
const { projectName, pages } = await reader.api({ locale }).pages.listBySlug({ projectUuid });
|
|
158
|
+
const records = createRecordLookup(options, reader, { locale });
|
|
153
159
|
return {
|
|
154
160
|
locale,
|
|
155
161
|
projectName,
|
|
@@ -163,10 +169,7 @@ async function respondWithLiveWebsite(options, name, respond, { everyLocale = fa
|
|
|
163
169
|
let locales;
|
|
164
170
|
let localizations;
|
|
165
171
|
try {
|
|
166
|
-
locales = everyLocale ? [...await
|
|
167
|
-
apiUrl,
|
|
168
|
-
projectUuid
|
|
169
|
-
})].sort((a, b) => Number(b.isDefault) - Number(a.isDefault)) : [UNNAMED_DEFAULT_LOCALE];
|
|
172
|
+
locales = everyLocale ? [...await reader.locales()].sort((a, b) => Number(b.isDefault) - Number(a.isDefault)) : [UNNAMED_DEFAULT_LOCALE];
|
|
170
173
|
localizations = await Promise.all(locales.map(readLocale));
|
|
171
174
|
} catch (error) {
|
|
172
175
|
const discovery = error instanceof RouteDiscoveryError;
|
|
@@ -242,15 +245,6 @@ ${entries}
|
|
|
242
245
|
</urlset>`, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
|
|
243
246
|
}, { everyLocale: true });
|
|
244
247
|
}
|
|
245
|
-
/** The request's member project, if its session is valid. Without an API, nobody is a member. */
|
|
246
|
-
async function resolveRequestMember(request, options) {
|
|
247
|
-
if (!options.apiUrl || !options.projectUuid) return { clearAuthCookie: false };
|
|
248
|
-
return resolveMemberProject({
|
|
249
|
-
apiUrl: options.apiUrl,
|
|
250
|
-
authCookieHeader: getServerAuthCookieHeader(request.headers),
|
|
251
|
-
projectUuid: options.projectUuid
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
248
|
/**
|
|
255
249
|
* The first discovered page of a derived layout, which the Canvas previews by default. With
|
|
256
250
|
* `recordId`, the page rendering that record, which the Studio follows when its slug changes.
|
|
@@ -265,21 +259,17 @@ async function createRoutesResponse(request, options) {
|
|
|
265
259
|
const recordId = searchParams.get("recordId");
|
|
266
260
|
const layout = camoxApp.getLayoutById(layoutId);
|
|
267
261
|
if (layout?._internal.kind !== "derived") return new Response("Not found", { status: 404 });
|
|
268
|
-
const
|
|
262
|
+
const reader = await resolveReader(request, options);
|
|
269
263
|
const headers = {
|
|
270
|
-
|
|
271
|
-
Vary: `Cookie, ${LOCALE_HEADER}
|
|
272
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
|
|
264
|
+
...readerHeaders(reader, { alwaysPrivate: true }),
|
|
265
|
+
Vary: `Cookie, ${LOCALE_HEADER}`
|
|
273
266
|
};
|
|
274
267
|
const tag = request.headers.get(LOCALE_HEADER);
|
|
275
268
|
try {
|
|
276
|
-
const pages = await discoverLayoutPages(layout, createLayoutRequestContext(options,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
isDefault: false
|
|
281
|
-
} : void 0
|
|
282
|
-
}));
|
|
269
|
+
const pages = await discoverLayoutPages(layout, createLayoutRequestContext(options, reader, { locale: tag ? {
|
|
270
|
+
tag,
|
|
271
|
+
isDefault: false
|
|
272
|
+
} : void 0 }));
|
|
283
273
|
const page = recordId ? pages.find((entry) => entry.recordId === recordId) : pages[0];
|
|
284
274
|
return Response.json({ path: page?.path ?? null }, { headers });
|
|
285
275
|
} catch (error) {
|
|
@@ -313,7 +303,7 @@ async function createOgResponse(request, options) {
|
|
|
313
303
|
source.context,
|
|
314
304
|
style
|
|
315
305
|
])),
|
|
316
|
-
"Cache-Control":
|
|
306
|
+
"Cache-Control": isPrivateRequest(request) ? PRIVATE_HEADERS["Cache-Control"] : "public, no-cache"
|
|
317
307
|
};
|
|
318
308
|
if (namesEtag(request, headers.ETag)) return new Response(null, {
|
|
319
309
|
status: 304,
|
|
@@ -329,66 +319,38 @@ async function readPageImageSource(request, options, app) {
|
|
|
329
319
|
const { apiUrl, projectUuid } = options;
|
|
330
320
|
const path = new URL(request.url).searchParams.get("path");
|
|
331
321
|
if (!apiUrl || !projectUuid || !path?.startsWith("/")) return null;
|
|
332
|
-
const
|
|
322
|
+
const requestReader = await resolveReader(request, options);
|
|
323
|
+
const located = await locateRequest(requestReader, options, path);
|
|
333
324
|
if (matchLocalizedRoute(located.path)?.kind !== "page") return null;
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
const authCookieHeader = member ? getServerAuthCookieHeader(request.headers) : void 0;
|
|
337
|
-
const requestedDraft = getRequestDraftId(request);
|
|
338
|
-
const api = (draftId) => createServerApiClient(apiUrl, {
|
|
339
|
-
authCookieHeader,
|
|
325
|
+
const reader = pageReaderOf(options, requestReader, located.locale);
|
|
326
|
+
const api = (draftId) => requestReader.api({
|
|
340
327
|
draftId,
|
|
341
328
|
locale: located.locale
|
|
342
329
|
});
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
330
|
+
const shown = await readPage({
|
|
331
|
+
app,
|
|
332
|
+
path: located.path,
|
|
333
|
+
pathname: path,
|
|
334
|
+
reader,
|
|
335
|
+
curated: () => reader.inScope((draftId) => api(draftId).pages.getByPath({
|
|
336
|
+
path: located.path,
|
|
337
|
+
projectUuid
|
|
338
|
+
})),
|
|
339
|
+
layout: (draftId, layoutId) => api(draftId).layouts.get({
|
|
348
340
|
projectUuid,
|
|
349
|
-
layoutId
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const record = asCollectionItem(result);
|
|
357
|
-
const collection = record && app.getCollectionById(record.collectionId);
|
|
358
|
-
return {
|
|
359
|
-
layout,
|
|
360
|
-
context: buildLayoutContext(layout, {
|
|
361
|
-
path,
|
|
362
|
-
locale: located.locale.tag,
|
|
363
|
-
projectName: shared.projectName,
|
|
364
|
-
data: result?.data,
|
|
365
|
-
params: match.params,
|
|
366
|
-
record: record && collection ? recordPageValues(collection, record) : void 0
|
|
367
|
-
})
|
|
368
|
-
};
|
|
341
|
+
layoutId
|
|
342
|
+
})
|
|
343
|
+
});
|
|
344
|
+
if (shown.kind === "none") return null;
|
|
345
|
+
if (shown.kind === "layout") return {
|
|
346
|
+
layout: shown.layout._internal,
|
|
347
|
+
context: shown.context
|
|
369
348
|
};
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
path: located.path,
|
|
375
|
-
projectUuid
|
|
376
|
-
}));
|
|
377
|
-
return {
|
|
378
|
-
layout: page.layout ? app.getLayoutById(page.layout.layoutId)?._internal : void 0,
|
|
379
|
-
context: curatedLayoutContext(page, located.locale)
|
|
380
|
-
};
|
|
381
|
-
} catch (error) {
|
|
382
|
-
if (isNotFoundError(error) && match) return codeOwned(match);
|
|
383
|
-
throw error;
|
|
384
|
-
}
|
|
349
|
+
const { page } = shown;
|
|
350
|
+
return {
|
|
351
|
+
layout: page.layout ? app.getLayoutById(page.layout.layoutId)?._internal : void 0,
|
|
352
|
+
context: curatedLayoutContext(page, located.locale)
|
|
385
353
|
};
|
|
386
|
-
try {
|
|
387
|
-
return match?.layout._internal.kind === "singleton" ? await codeOwned(match) : await curated();
|
|
388
|
-
} catch (error) {
|
|
389
|
-
if (isNotFoundError(error)) return null;
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
354
|
}
|
|
393
355
|
function createDefaultHeadInput(stylesheetUrl) {
|
|
394
356
|
return {
|
|
@@ -491,17 +453,19 @@ function renderBlockPreloads(options, dehydratedState) {
|
|
|
491
453
|
* is only ever the studio's, so no shared cache keeps it.
|
|
492
454
|
*/
|
|
493
455
|
function createRendererDocumentResponse(options) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
456
|
+
return htmlResponse(htmlDocument({
|
|
457
|
+
before: `<meta charset="utf-8"><meta name="robots" content="noindex"><title>Camox renderer</title>`,
|
|
458
|
+
data: {
|
|
459
|
+
apiUrl: options.apiUrl,
|
|
460
|
+
authenticationUrl: options.authenticationUrl,
|
|
461
|
+
projectUuid: options.projectUuid,
|
|
462
|
+
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
|
|
463
|
+
},
|
|
464
|
+
entryUrl: options.rendererClientEntryUrl
|
|
465
|
+
}), {
|
|
466
|
+
...PRIVATE_HEADERS,
|
|
503
467
|
"X-Robots-Tag": "noindex"
|
|
504
|
-
}
|
|
468
|
+
});
|
|
505
469
|
}
|
|
506
470
|
/**
|
|
507
471
|
* Marks the look document ready (`data-camox-ready` on its root element) for the browser to wait
|
|
@@ -515,7 +479,7 @@ function createRendererDocumentResponse(options) {
|
|
|
515
479
|
const LOOK_READY_SCRIPT = `(()=>{const loaded=new WeakSet();let hydrated=false,marked=false;const mark=()=>{if(marked||!hydrated||document.readyState!=="complete")return;const frames=[...document.querySelectorAll("iframe")];for(const frame of frames)if(frame.getAttribute("loading")==="lazy")frame.setAttribute("loading","eager");if(!frames.every((frame)=>loaded.has(frame)))return;marked=true;requestAnimationFrame(()=>requestAnimationFrame(()=>document.documentElement.setAttribute("data-camox-ready","")))};const settle=(event)=>{if(event.target&&event.target.tagName==="IFRAME")loaded.add(event.target);mark()};document.addEventListener("load",settle,true);document.addEventListener("error",settle,true);addEventListener("load",mark,{once:true});addEventListener("camox:hydrated",()=>{hydrated=true;mark()},{once:true})})()`;
|
|
516
480
|
/** The headers every look document answer carries: private, never cached, never indexed. */
|
|
517
481
|
const LOOK_HEADERS = {
|
|
518
|
-
|
|
482
|
+
...PRIVATE_HEADERS,
|
|
519
483
|
"X-Robots-Tag": "noindex"
|
|
520
484
|
};
|
|
521
485
|
/** What the look document answers when it shows nothing: no token, or one the API refused. */
|
|
@@ -566,9 +530,9 @@ function overrideParam(searchParams, name) {
|
|
|
566
530
|
async function createLookDocumentResponse(request, options) {
|
|
567
531
|
const { apiUrl, authenticationUrl, projectUuid, renderPage } = options;
|
|
568
532
|
const { searchParams } = new URL(request.url);
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
533
|
+
const reader = lookReader(request, options);
|
|
534
|
+
if (!reader || !apiUrl || !authenticationUrl || !projectUuid || !renderPage) return lookNotFound();
|
|
535
|
+
const draftId = reader.draftId;
|
|
572
536
|
const tag = searchParams.get(LOOK_LOCALE_PARAM)?.toLowerCase();
|
|
573
537
|
const blockDefinition = searchParams.get(LOOK_BLOCK_DEFINITION_PARAM);
|
|
574
538
|
const layoutDefinition = searchParams.get(LOOK_LAYOUT_DEFINITION_PARAM);
|
|
@@ -577,15 +541,12 @@ async function createLookDocumentResponse(request, options) {
|
|
|
577
541
|
let locale;
|
|
578
542
|
let shown;
|
|
579
543
|
try {
|
|
580
|
-
locales = await
|
|
581
|
-
lookToken,
|
|
582
|
-
draftId
|
|
583
|
-
}).locales.list({ projectUuid });
|
|
544
|
+
locales = await reader.locales();
|
|
584
545
|
locale = locales.find((each) => tag ? each.tag.toLowerCase() === tag : each.isDefault);
|
|
585
546
|
if (!locale) return lookNotFound();
|
|
586
547
|
const reading = {
|
|
587
548
|
apiUrl,
|
|
588
|
-
|
|
549
|
+
reader,
|
|
589
550
|
draftId,
|
|
590
551
|
locale,
|
|
591
552
|
projectUuid,
|
|
@@ -613,27 +574,16 @@ async function createLookDocumentResponse(request, options) {
|
|
|
613
574
|
locales
|
|
614
575
|
};
|
|
615
576
|
const appHtml = await renderPage(input);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
${renderBlockPreloads(options, dehydratedState)}
|
|
624
|
-
<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(input, appHtml))}<\/script>
|
|
625
|
-
</head>
|
|
626
|
-
<body${renderedHead.bodyAttrs}>
|
|
627
|
-
${renderedHead.bodyTagsOpen}
|
|
628
|
-
<div id="root">${appHtml}</div>
|
|
629
|
-
${renderedHead.bodyTags}
|
|
630
|
-
${renderClientEntryScript(options.pageClientEntryUrl)}
|
|
631
|
-
</body>
|
|
632
|
-
</html>`, { headers: {
|
|
577
|
+
return htmlResponse(htmlDocument({
|
|
578
|
+
head: renderHead(document, createPageHeadInput(shown.head), options.stylesheetUrl, void 0, locale),
|
|
579
|
+
after: `<meta name="robots" content="noindex"><script>${LOOK_READY_SCRIPT}<\/script>${renderBlockPreloads(options, dehydratedState)}`,
|
|
580
|
+
data: hydrationInputFor(input, appHtml),
|
|
581
|
+
root: appHtml,
|
|
582
|
+
entryUrl: options.pageClientEntryUrl
|
|
583
|
+
}), {
|
|
633
584
|
...LOOK_HEADERS,
|
|
634
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
635
585
|
"Referrer-Policy": "no-referrer"
|
|
636
|
-
}
|
|
586
|
+
});
|
|
637
587
|
}
|
|
638
588
|
/** Where the look document's client believes it is: `path` of the draft, in its Locale. */
|
|
639
589
|
function lookHref(request, options, pathname, draftId) {
|
|
@@ -642,65 +592,38 @@ function lookHref(request, options, pathname, draftId) {
|
|
|
642
592
|
return href;
|
|
643
593
|
}
|
|
644
594
|
/**
|
|
645
|
-
* The look document's page at `path`, with its head, as a visitor would see it
|
|
646
|
-
*
|
|
647
|
-
*
|
|
595
|
+
* The look document's page at `path`, with its head, as a visitor would see it: whatever Page
|
|
596
|
+
* Read finds there, read with the look token in its draft. A Layout's route whose loader finds
|
|
597
|
+
* nothing answers why.
|
|
648
598
|
*/
|
|
649
599
|
async function lookAtPage(request, options, reading) {
|
|
650
|
-
const { apiUrl, draftId, locale } = reading;
|
|
600
|
+
const { apiUrl, draftId, locale, projectUuid, queryClient } = reading;
|
|
651
601
|
const path = normalizePagePath(new URL(request.url).searchParams.get(LOOK_PATH_PARAM));
|
|
652
|
-
const camoxApp = await options.getCamoxApp?.();
|
|
653
|
-
const match = camoxApp && matchDerivedLayout(camoxApp.getLayouts(), path);
|
|
654
|
-
if (match?.layout._internal.kind === "singleton") return lookAtLayoutPage(request, options, reading, path, match);
|
|
655
|
-
let page;
|
|
656
|
-
try {
|
|
657
|
-
page = await loadCamoxPage({
|
|
658
|
-
...reading,
|
|
659
|
-
pathname: path
|
|
660
|
-
});
|
|
661
|
-
} catch (error) {
|
|
662
|
-
if (match && isNotFoundError(error)) return lookAtLayoutPage(request, options, reading, path, match);
|
|
663
|
-
throw error;
|
|
664
|
-
}
|
|
665
|
-
const pathname = localizedPath(path, locale);
|
|
666
|
-
const href = lookHref(request, options, pathname, draftId);
|
|
667
|
-
const loaderData = buildCamoxPageLoaderData(apiUrl, page, href.origin);
|
|
668
|
-
return {
|
|
669
|
-
head: camoxApp ? buildCamoxPageHead(camoxApp, loaderData, locale) : {},
|
|
670
|
-
href: href.href,
|
|
671
|
-
layoutIdentity: getLayoutIdentity(page.layout),
|
|
672
|
-
loaderData,
|
|
673
|
-
pathname
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* The look document's page of a singleton or derived layout at `path`: the layout's blocks, read
|
|
678
|
-
* with the look token, around what its loader returns, its records read from the draft. A loader
|
|
679
|
-
* that finds nothing answers why.
|
|
680
|
-
*/
|
|
681
|
-
async function lookAtLayoutPage(request, options, reading, path, match) {
|
|
682
|
-
const { apiUrl, lookToken, draftId, locale, projectUuid, queryClient } = reading;
|
|
683
602
|
const app = await options.getCamoxApp?.();
|
|
684
|
-
const layout = match.layout._internal;
|
|
685
603
|
const pathname = localizedPath(path, locale);
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
604
|
+
const api = (inDraft) => reading.reader.api({
|
|
605
|
+
draftId: inDraft,
|
|
606
|
+
locale
|
|
607
|
+
});
|
|
608
|
+
const reader = pageReaderOf(options, reading.reader, locale);
|
|
609
|
+
const shown = await readPage({
|
|
610
|
+
app,
|
|
611
|
+
path,
|
|
612
|
+
pathname,
|
|
613
|
+
reader,
|
|
614
|
+
curated: () => reader.inScope((inDraft) => loadCamoxPage({
|
|
615
|
+
...reading,
|
|
616
|
+
draftId: inDraft,
|
|
617
|
+
pathname: path
|
|
618
|
+
})),
|
|
619
|
+
layout: (inDraft, layoutId) => api(inDraft).layouts.get({
|
|
698
620
|
projectUuid,
|
|
699
|
-
layoutId
|
|
700
|
-
})
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
621
|
+
layoutId
|
|
622
|
+
})
|
|
623
|
+
});
|
|
624
|
+
if (shown.kind === "none") {
|
|
625
|
+
if (!shown.layout) return lookNotFound();
|
|
626
|
+
return new Response(`The draft has no page at ${path}: the ${shown.layout._internal.id} layout's loader found nothing to show.`, {
|
|
704
627
|
status: 404,
|
|
705
628
|
headers: {
|
|
706
629
|
...LOOK_HEADERS,
|
|
@@ -708,58 +631,84 @@ async function lookAtLayoutPage(request, options, reading, path, match) {
|
|
|
708
631
|
}
|
|
709
632
|
});
|
|
710
633
|
}
|
|
711
|
-
const
|
|
634
|
+
const href = lookHref(request, options, pathname, draftId);
|
|
635
|
+
if (shown.kind === "curated") return curatedPageShown({
|
|
636
|
+
app,
|
|
712
637
|
apiUrl,
|
|
713
|
-
|
|
714
|
-
locale
|
|
638
|
+
page: shown.page,
|
|
639
|
+
locale,
|
|
640
|
+
href: href.href,
|
|
641
|
+
pathname
|
|
715
642
|
});
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
layoutId: layout.id
|
|
721
|
-
}), shared, inDraft);
|
|
722
|
-
studioCache.set(reads.derivedPageData({
|
|
643
|
+
landLayoutPage(createServerStudioCache(queryClient, {
|
|
644
|
+
reader: reading.reader,
|
|
645
|
+
locale
|
|
646
|
+
}), shown, {
|
|
723
647
|
projectUuid,
|
|
724
|
-
|
|
648
|
+
options,
|
|
649
|
+
pathname
|
|
650
|
+
}, { in: draftId });
|
|
651
|
+
return layoutPageShown({
|
|
652
|
+
page: shown,
|
|
653
|
+
apiUrl,
|
|
654
|
+
href: href.href,
|
|
725
655
|
pathname
|
|
726
|
-
}), result ?? null, inDraft);
|
|
727
|
-
const href = lookHref(request, options, pathname, draftId);
|
|
728
|
-
const record = asCollectionItem(result);
|
|
729
|
-
const collection = record && app?.getCollectionById(record.collectionId);
|
|
730
|
-
const context = buildLayoutContext(layout, {
|
|
731
|
-
path: pathname,
|
|
732
|
-
locale: locale.tag,
|
|
733
|
-
projectName: shared.projectName,
|
|
734
|
-
data: result?.data,
|
|
735
|
-
params: match.params,
|
|
736
|
-
record: record && collection ? recordPageValues(collection, record) : void 0
|
|
737
656
|
});
|
|
657
|
+
}
|
|
658
|
+
/** What a render shows of a curated page. */
|
|
659
|
+
function curatedPageShown({ app, apiUrl, page, locale, href, pathname }) {
|
|
660
|
+
const loaderData = buildCamoxPageLoaderData(apiUrl, page, new URL(href).origin);
|
|
661
|
+
return {
|
|
662
|
+
head: app ? buildCamoxPageHead(app, loaderData, locale) : {},
|
|
663
|
+
href,
|
|
664
|
+
layoutIdentity: getLayoutIdentity(page.layout),
|
|
665
|
+
loaderData,
|
|
666
|
+
pathname
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* What a render shows of a Layout Page: its head from its layout's options, and the layout, its
|
|
671
|
+
* loader's result and its route's params, which its client renders it from.
|
|
672
|
+
*/
|
|
673
|
+
function layoutPageShown({ page, apiUrl, href, pathname }) {
|
|
674
|
+
const layout = page.layout._internal;
|
|
738
675
|
return {
|
|
739
676
|
head: buildPageHead({
|
|
740
677
|
layout,
|
|
741
|
-
context,
|
|
742
|
-
origin: href.origin,
|
|
743
|
-
faviconUrl: buildFaviconUrl(apiUrl,
|
|
678
|
+
context: page.context,
|
|
679
|
+
origin: new URL(href).origin,
|
|
680
|
+
faviconUrl: buildFaviconUrl(apiUrl, page.blocks.project),
|
|
744
681
|
hasMarkdown: layout.toMarkdown !== void 0,
|
|
745
|
-
recordType: record?.structuredData?.node?.["@type"]
|
|
682
|
+
recordType: page.record?.item.structuredData?.node?.["@type"]
|
|
746
683
|
}),
|
|
747
|
-
href
|
|
748
|
-
layoutIdentity: getLayoutIdentity(
|
|
684
|
+
href,
|
|
685
|
+
layoutIdentity: getLayoutIdentity(page.blocks.layout),
|
|
749
686
|
loaderData: null,
|
|
750
687
|
pathname,
|
|
751
688
|
derived: {
|
|
752
689
|
layoutId: layout.id,
|
|
753
690
|
page: {
|
|
754
|
-
params:
|
|
691
|
+
params: page.params,
|
|
755
692
|
path: pathname,
|
|
756
|
-
projectName:
|
|
693
|
+
projectName: page.blocks.projectName
|
|
757
694
|
},
|
|
758
|
-
result,
|
|
759
|
-
layout:
|
|
695
|
+
result: page.result,
|
|
696
|
+
layout: page.blocks.layout
|
|
760
697
|
}
|
|
761
698
|
};
|
|
762
699
|
}
|
|
700
|
+
/** Lands a Layout Page's reads in the studio's cache, which its client reads them from. */
|
|
701
|
+
function landLayoutPage(studioCache, page, { projectUuid, options, pathname }, inDraft) {
|
|
702
|
+
studioCache.land(reads.layoutStructure({
|
|
703
|
+
projectUuid,
|
|
704
|
+
layoutId: page.layout._internal.id
|
|
705
|
+
}), page.blocks, inDraft);
|
|
706
|
+
studioCache.set(reads.derivedPageData({
|
|
707
|
+
projectUuid,
|
|
708
|
+
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
709
|
+
pathname
|
|
710
|
+
}), page.result ?? null, inDraft);
|
|
711
|
+
}
|
|
763
712
|
/**
|
|
764
713
|
* The look document's Block Definition mode: the block of the Block Definition `type` as the
|
|
765
714
|
* draft would show it placed, its content and Block Settings empty but for the overrides the URL
|
|
@@ -768,22 +717,20 @@ async function lookAtLayoutPage(request, options, reading, path, match) {
|
|
|
768
717
|
* as a page's block does; its page is the Locale's home, which links resolve against.
|
|
769
718
|
*/
|
|
770
719
|
async function lookAtBlockDefinition(request, options, reading, type) {
|
|
771
|
-
const {
|
|
720
|
+
const { reader, draftId, locale, projectUuid, queryClient } = reading;
|
|
772
721
|
const { searchParams } = new URL(request.url);
|
|
773
722
|
const content = overrideParam(searchParams, LOOK_CONTENT_PARAM);
|
|
774
723
|
const settings = overrideParam(searchParams, LOOK_SETTINGS_PARAM);
|
|
775
724
|
if (!content.ok) return lookRefused(content.error);
|
|
776
725
|
if (!settings.ok) return lookRefused(settings.error);
|
|
777
726
|
const studioCache = createServerStudioCache(queryClient, {
|
|
778
|
-
|
|
779
|
-
lookToken,
|
|
727
|
+
reader,
|
|
780
728
|
locale
|
|
781
729
|
});
|
|
782
730
|
const inDraft = { in: draftId };
|
|
783
731
|
let bundle;
|
|
784
732
|
try {
|
|
785
|
-
[bundle] = await Promise.all([
|
|
786
|
-
lookToken,
|
|
733
|
+
[bundle] = await Promise.all([reader.api({
|
|
787
734
|
draftId,
|
|
788
735
|
locale
|
|
789
736
|
}).blocks.getUnplaced({
|
|
@@ -815,22 +762,20 @@ async function lookAtBlockDefinition(request, options, reading, type) {
|
|
|
815
762
|
* says. Its path is the Locale's home, which links resolve against.
|
|
816
763
|
*/
|
|
817
764
|
async function lookAtLayoutDefinition(request, options, reading, layoutId) {
|
|
818
|
-
const {
|
|
765
|
+
const { reader, draftId, locale, projectUuid, queryClient } = reading;
|
|
819
766
|
const { searchParams } = new URL(request.url);
|
|
820
767
|
const blocks = overrideParam(searchParams, LOOK_BLOCKS_PARAM);
|
|
821
768
|
if (!blocks.ok) return lookRefused(blocks.error);
|
|
822
769
|
const placeholder = searchParams.get(LOOK_PLACEHOLDER_PARAM) ?? LOOK_PLACEHOLDER_HEIGHTS[0];
|
|
823
770
|
if (!LOOK_PLACEHOLDER_HEIGHTS.includes(placeholder)) return lookRefused(`The placeholder is ${LOOK_PLACEHOLDER_HEIGHTS.join(" or ")}.`);
|
|
824
771
|
const studioCache = createServerStudioCache(queryClient, {
|
|
825
|
-
|
|
826
|
-
lookToken,
|
|
772
|
+
reader,
|
|
827
773
|
locale
|
|
828
774
|
});
|
|
829
775
|
const inDraft = { in: draftId };
|
|
830
776
|
let shared;
|
|
831
777
|
try {
|
|
832
|
-
[shared] = await Promise.all([
|
|
833
|
-
lookToken,
|
|
778
|
+
[shared] = await Promise.all([reader.api({
|
|
834
779
|
draftId,
|
|
835
780
|
locale
|
|
836
781
|
}).layouts.getUnplaced({
|
|
@@ -871,6 +816,38 @@ function renderClientEntryScript(src) {
|
|
|
871
816
|
if (!src) return "";
|
|
872
817
|
return `<script type="module" src="${escapeXml(src)}"><\/script>`;
|
|
873
818
|
}
|
|
819
|
+
const NO_HEAD = {
|
|
820
|
+
htmlAttrs: "",
|
|
821
|
+
headTags: "",
|
|
822
|
+
bodyAttrs: "",
|
|
823
|
+
bodyTagsOpen: "",
|
|
824
|
+
bodyTags: ""
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Every document the runtime serves: its head, between what comes `before` and `after` it, the
|
|
828
|
+
* render input its client starts from (`__CAMOX_DATA__`), the app's HTML in its root, if it has
|
|
829
|
+
* one, and the client's entry script.
|
|
830
|
+
*/
|
|
831
|
+
function htmlDocument({ head = NO_HEAD, before = "", after = "", data, root, entryUrl }) {
|
|
832
|
+
return `<!doctype html>
|
|
833
|
+
<html${head.htmlAttrs}>
|
|
834
|
+
<head>
|
|
835
|
+
${before}${head.headTags}${after}
|
|
836
|
+
<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(data)}<\/script>
|
|
837
|
+
</head>
|
|
838
|
+
<body${head.bodyAttrs}>
|
|
839
|
+
${head.bodyTagsOpen}${root === void 0 ? "" : `<div id="root">${root}</div>`}
|
|
840
|
+
${head.bodyTags}
|
|
841
|
+
${renderClientEntryScript(entryUrl)}
|
|
842
|
+
</body>
|
|
843
|
+
</html>`;
|
|
844
|
+
}
|
|
845
|
+
/** An HTML document's response. */
|
|
846
|
+
function htmlResponse(html, headers) {
|
|
847
|
+
const response = new Response(html, { headers });
|
|
848
|
+
response.headers.set("Content-Type", "text/html; charset=utf-8");
|
|
849
|
+
return response;
|
|
850
|
+
}
|
|
874
851
|
/**
|
|
875
852
|
* The site document the studio's frames load: the site's head and an empty root, which the
|
|
876
853
|
* renderer frame renders the page into. The studio never renders the site's code itself.
|
|
@@ -981,7 +958,7 @@ async function createSignInResponse(request, apiUrl) {
|
|
|
981
958
|
status: 302,
|
|
982
959
|
headers: {
|
|
983
960
|
Location: `${url.pathname}${url.search}`,
|
|
984
|
-
|
|
961
|
+
...PRIVATE_HEADERS,
|
|
985
962
|
"Referrer-Policy": "no-referrer",
|
|
986
963
|
"Set-Cookie": buildServerAuthCookieHeader(session, { secure: url.protocol === "https:" })
|
|
987
964
|
}
|
|
@@ -1063,15 +1040,10 @@ function withLocatedTags(head, { url, locale, alternates }) {
|
|
|
1063
1040
|
}]
|
|
1064
1041
|
};
|
|
1065
1042
|
}
|
|
1066
|
-
function requestLocaleReads(options, {
|
|
1043
|
+
function requestLocaleReads(options, { reader, draftId, queryClient }) {
|
|
1067
1044
|
const { apiUrl, projectUuid } = options;
|
|
1068
|
-
const reading = {
|
|
1069
|
-
authCookieHeader: draftId ? authCookieHeader : void 0,
|
|
1070
|
-
draftId
|
|
1071
|
-
};
|
|
1072
|
-
const source = draftId ? "draft" : "live";
|
|
1073
1045
|
const pagesByLocale = /* @__PURE__ */ new Map();
|
|
1074
|
-
const studioCache =
|
|
1046
|
+
const studioCache = queryClient ? createServerStudioCache(queryClient, { reader }) : void 0;
|
|
1075
1047
|
return {
|
|
1076
1048
|
pages: (locale) => {
|
|
1077
1049
|
if (!apiUrl || !projectUuid) return Promise.resolve([]);
|
|
@@ -1080,19 +1052,19 @@ function requestLocaleReads(options, { authCookieHeader, draftId, queryClient })
|
|
|
1080
1052
|
locale
|
|
1081
1053
|
});
|
|
1082
1054
|
if (loaded) return Promise.resolve(loaded);
|
|
1083
|
-
const read = pagesByLocale.get(locale.tag) ??
|
|
1084
|
-
|
|
1055
|
+
const read = pagesByLocale.get(locale.tag) ?? reader.api({
|
|
1056
|
+
draftId,
|
|
1085
1057
|
locale
|
|
1086
1058
|
}).pages.listBySlug({ projectUuid }).then(({ pages }) => pages);
|
|
1087
1059
|
pagesByLocale.set(locale.tag, read);
|
|
1088
1060
|
return read;
|
|
1089
1061
|
},
|
|
1090
|
-
context: (locale) => createLayoutRequestContext(options,
|
|
1091
|
-
|
|
1062
|
+
context: (locale) => createLayoutRequestContext(options, reader, {
|
|
1063
|
+
draftId,
|
|
1092
1064
|
locale
|
|
1093
1065
|
}),
|
|
1094
|
-
records: (locale) => createRecordLookup(options,
|
|
1095
|
-
|
|
1066
|
+
records: (locale) => createRecordLookup(options, reader, {
|
|
1067
|
+
draftId,
|
|
1096
1068
|
locale
|
|
1097
1069
|
})
|
|
1098
1070
|
};
|
|
@@ -1167,71 +1139,116 @@ async function createPreviewHandoffResponse({ options, pathname, request }) {
|
|
|
1167
1139
|
layoutIdentity: null,
|
|
1168
1140
|
dehydratedState: dehydrate(new QueryClient())
|
|
1169
1141
|
};
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1142
|
+
return htmlResponse(htmlDocument({
|
|
1143
|
+
before: `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="referrer" content="no-referrer"><title>Camox draft preview</title>`,
|
|
1144
|
+
data: input,
|
|
1145
|
+
root: await options.renderPage(input),
|
|
1146
|
+
entryUrl: options.pageClientEntryUrl
|
|
1147
|
+
}), {
|
|
1148
|
+
...PRIVATE_HEADERS,
|
|
1174
1149
|
"Referrer-Policy": "no-referrer"
|
|
1175
|
-
}
|
|
1150
|
+
});
|
|
1176
1151
|
}
|
|
1177
|
-
/**
|
|
1178
|
-
|
|
1152
|
+
/**
|
|
1153
|
+
* What Page Read needs of the request's Reader, in one Locale. Only a Member's Draft can be
|
|
1154
|
+
* refused, so only then does the first read settle the Draft; anyone else reads one scope
|
|
1155
|
+
* throughout: a Visitor Live, a look token its own Draft.
|
|
1156
|
+
*/
|
|
1157
|
+
function pageReaderOf(options, reader, locale) {
|
|
1158
|
+
return {
|
|
1159
|
+
locale,
|
|
1160
|
+
inScope: (read) => reader.read(read),
|
|
1161
|
+
...!(reader.member && reader.draftId) && { draft: { id: reader.draftId } },
|
|
1162
|
+
records: (draftId) => createRecordLookup(options, reader, {
|
|
1163
|
+
draftId,
|
|
1164
|
+
locale
|
|
1165
|
+
})
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
/** Starts reading the curated page at a locale-relative path, as the request's Reader reads it. */
|
|
1169
|
+
function readCuratedPage(options, reader, { locale, path }) {
|
|
1179
1170
|
const queryClient = new QueryClient();
|
|
1180
1171
|
return {
|
|
1181
1172
|
queryClient,
|
|
1182
|
-
result:
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
draftId: getRequestDraftId(request),
|
|
1173
|
+
result: reader.read((draftId) => loadCamoxPage({
|
|
1174
|
+
reader,
|
|
1175
|
+
draftId,
|
|
1186
1176
|
locale,
|
|
1187
|
-
origin: new URL(request.url).origin,
|
|
1188
1177
|
pathname: path,
|
|
1189
1178
|
projectUuid: options.projectUuid,
|
|
1190
1179
|
queryClient
|
|
1191
|
-
})
|
|
1180
|
+
}))
|
|
1192
1181
|
};
|
|
1193
1182
|
}
|
|
1194
1183
|
/** A first path segment that could be a locale's prefix: a language tag, in lowercase. */
|
|
1195
|
-
const LOCALE_PREFIX_LIKE = /^\/[a-z]{2,3}(?:-[a-z0-9]{1,8})*(?:\/|$)/;
|
|
1184
|
+
const LOCALE_PREFIX_LIKE = /^\/([a-z]{2,3}(?:-[a-z0-9]{1,8})*)(?:\/|$)/;
|
|
1196
1185
|
/**
|
|
1197
|
-
* The read of the curated page a path
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1186
|
+
* The read of the curated page a path most likely names, started while the request's locales are
|
|
1187
|
+
* read, so the page's read needn't wait for them: when its first segment names a language, as a
|
|
1188
|
+
* locale's prefix does, in that language's locale, at the rest of the path; otherwise in the
|
|
1189
|
+
* default locale, at the path. A locale's prefix is its tag in lowercase. Once read, the locales
|
|
1190
|
+
* confirm the guess, or the read is left unused. A path that never shows a curated page reads
|
|
1191
|
+
* none.
|
|
1203
1192
|
*/
|
|
1204
|
-
async function
|
|
1193
|
+
async function readPageEarly(options, reader, pathname) {
|
|
1205
1194
|
const { apiUrl, projectUuid } = options;
|
|
1206
1195
|
if (!apiUrl || !projectUuid) return void 0;
|
|
1207
|
-
|
|
1196
|
+
const prefixed = LOCALE_PREFIX_LIKE.exec(pathname);
|
|
1197
|
+
const language = prefixed && languageTagOf(prefixed[1]);
|
|
1198
|
+
const guessed = language ? {
|
|
1199
|
+
locale: {
|
|
1200
|
+
tag: language,
|
|
1201
|
+
isDefault: false
|
|
1202
|
+
},
|
|
1203
|
+
path: pathname.slice(prefixed[0].replace(/\/$/, "").length) || "/"
|
|
1204
|
+
} : {
|
|
1205
|
+
locale: UNNAMED_DEFAULT_LOCALE,
|
|
1206
|
+
path: pathname
|
|
1207
|
+
};
|
|
1208
|
+
if (matchLocalizedRoute(guessed.path)?.kind !== "page") return void 0;
|
|
1208
1209
|
const app = await options.getCamoxApp?.().catch(() => void 0);
|
|
1209
|
-
if (
|
|
1210
|
+
if (!mayShowCuratedPage(app, guessed.path)) return void 0;
|
|
1210
1211
|
const read = readCuratedPage({
|
|
1211
1212
|
...options,
|
|
1212
|
-
apiUrl,
|
|
1213
1213
|
projectUuid
|
|
1214
|
-
},
|
|
1215
|
-
locale: UNNAMED_DEFAULT_LOCALE,
|
|
1216
|
-
path: pathname
|
|
1217
|
-
});
|
|
1214
|
+
}, reader, guessed);
|
|
1218
1215
|
read.result.catch(() => void 0);
|
|
1219
|
-
return
|
|
1216
|
+
return {
|
|
1217
|
+
locale: guessed.locale,
|
|
1218
|
+
read
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const languageNames = new Intl.DisplayNames(["en"], {
|
|
1222
|
+
type: "language",
|
|
1223
|
+
fallback: "none"
|
|
1224
|
+
});
|
|
1225
|
+
/** The canonical tag of the language a locale's prefix names, or null when it names none. */
|
|
1226
|
+
function languageTagOf(prefix) {
|
|
1227
|
+
try {
|
|
1228
|
+
const [tag] = Intl.getCanonicalLocales(prefix);
|
|
1229
|
+
return tag && languageNames.of(tag) ? tag : null;
|
|
1230
|
+
} catch {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/** The early read, when it's the request's page's: in the locale the request's path names. */
|
|
1235
|
+
function earlyReadIn(early, { locale }) {
|
|
1236
|
+
if (!early) return void 0;
|
|
1237
|
+
return (locale.isDefault ? early.locale.isDefault : early.locale.tag === locale.tag) ? early.read : void 0;
|
|
1220
1238
|
}
|
|
1221
1239
|
/**
|
|
1222
1240
|
* The reads of each locale the head of the request's curated page makes, and those it can make
|
|
1223
|
-
* while the page is read. When the
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1241
|
+
* while the page is read. When the Reader has no Draft to read, the request reads Live, so the
|
|
1242
|
+
* pages of the locales the page's read doesn't load start loading now. Otherwise they wait for
|
|
1243
|
+
* the page's read to settle the Draft.
|
|
1226
1244
|
*/
|
|
1227
|
-
function curatedPageLocaleReads(options,
|
|
1228
|
-
const authCookieHeader = getServerAuthCookieHeader(request.headers);
|
|
1245
|
+
function curatedPageLocaleReads(options, reader, located, queryClient) {
|
|
1229
1246
|
const forDraft = (draftId) => requestLocaleReads(options, {
|
|
1230
|
-
|
|
1247
|
+
reader,
|
|
1231
1248
|
draftId,
|
|
1232
1249
|
queryClient
|
|
1233
1250
|
});
|
|
1234
|
-
if (
|
|
1251
|
+
if (reader.draftId) return forDraft;
|
|
1235
1252
|
const live = forDraft(void 0);
|
|
1236
1253
|
if (located.locales.length > 1) for (const locale of located.locales) {
|
|
1237
1254
|
if (locale.tag === located.locale.tag || locale.isDefault) continue;
|
|
@@ -1239,119 +1256,189 @@ function curatedPageLocaleReads(options, request, located, queryClient) {
|
|
|
1239
1256
|
}
|
|
1240
1257
|
return () => live;
|
|
1241
1258
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1259
|
+
/**
|
|
1260
|
+
* A page's HTML, or with `dataOnly` its render input as JSON, which client navigation loads:
|
|
1261
|
+
* whatever Page Read finds at the path, as the request reads it. A runtime that can't render
|
|
1262
|
+
* pages answers their data.
|
|
1263
|
+
*/
|
|
1264
|
+
async function createPageResponse({ options, pathname, located, request, reader: requestReader, pageRead, dataOnly: dataRequested }) {
|
|
1265
|
+
const { apiUrl, projectUuid, renderPage } = options;
|
|
1266
|
+
if (!apiUrl || !projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
|
|
1267
|
+
const dataOnly = dataRequested || !options.authenticationUrl || !renderPage;
|
|
1268
|
+
const app = await options.getCamoxApp?.();
|
|
1269
|
+
const reader = pageReaderOf(options, requestReader, located.locale);
|
|
1270
|
+
const api = (draftId) => requestReader.api({
|
|
1271
|
+
draftId,
|
|
1272
|
+
locale: located.locale
|
|
1273
|
+
});
|
|
1274
|
+
const curatedRead = pageRead ?? {
|
|
1275
|
+
queryClient: new QueryClient(),
|
|
1276
|
+
result: void 0
|
|
1277
|
+
};
|
|
1278
|
+
let localeReads;
|
|
1279
|
+
const shown = await readPage({
|
|
1280
|
+
app,
|
|
1281
|
+
path: located.path,
|
|
1245
1282
|
pathname,
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1283
|
+
reader,
|
|
1284
|
+
curated: () => {
|
|
1285
|
+
localeReads = curatedPageLocaleReads(options, requestReader, located, curatedRead.queryClient);
|
|
1286
|
+
return curatedRead.result ?? reader.inScope((draftId) => loadCamoxPage({
|
|
1287
|
+
reader: requestReader,
|
|
1288
|
+
draftId,
|
|
1289
|
+
locale: located.locale,
|
|
1290
|
+
pathname: located.path,
|
|
1291
|
+
projectUuid,
|
|
1292
|
+
queryClient: curatedRead.queryClient
|
|
1293
|
+
}));
|
|
1294
|
+
},
|
|
1295
|
+
layout: (draftId, layoutId) => api(draftId).layouts.get({
|
|
1296
|
+
projectUuid,
|
|
1297
|
+
layoutId
|
|
1298
|
+
})
|
|
1249
1299
|
});
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1300
|
+
if (shown.kind === "none") {
|
|
1301
|
+
if (shown.layout) return new Response("Page not found", { status: 404 });
|
|
1302
|
+
return dataOnly ? Response.json({
|
|
1303
|
+
kind: "page",
|
|
1304
|
+
message: "No Camox page on this path",
|
|
1305
|
+
pathname
|
|
1306
|
+
}, { status: 404 }) : new Response("No Camox page on this path", { status: 404 });
|
|
1307
|
+
}
|
|
1308
|
+
const href = renderedHref(request.url, shown);
|
|
1309
|
+
const queryClient = shown.kind === "curated" ? curatedRead.queryClient : new QueryClient();
|
|
1310
|
+
let page;
|
|
1311
|
+
let readAlternatesOf;
|
|
1312
|
+
let site;
|
|
1313
|
+
if (shown.kind === "curated") {
|
|
1314
|
+
page = curatedPageShown({
|
|
1315
|
+
app,
|
|
1316
|
+
apiUrl,
|
|
1317
|
+
page: shown.page,
|
|
1318
|
+
locale: located.locale,
|
|
1319
|
+
href,
|
|
1320
|
+
pathname
|
|
1264
1321
|
});
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const camoxApp = await options.getCamoxApp?.();
|
|
1268
|
-
const document = await options.getDocument?.() ?? {};
|
|
1269
|
-
if (isHiddenFromSearchEngines(result.data.page.project)) headers.set("X-Robots-Tag", "noindex");
|
|
1270
|
-
const { alternates, head } = await readAlternates(request, options, {
|
|
1271
|
-
located,
|
|
1322
|
+
site = shown.page.project;
|
|
1323
|
+
readAlternatesOf = {
|
|
1272
1324
|
identity: {
|
|
1273
1325
|
kind: "curated",
|
|
1274
|
-
pageId:
|
|
1326
|
+
pageId: shown.page.page.id
|
|
1275
1327
|
},
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1328
|
+
described: curatedPageDescription(app, shown.page, located.locale),
|
|
1329
|
+
localeReads: localeReads(shown.draftId)
|
|
1330
|
+
};
|
|
1331
|
+
} else {
|
|
1332
|
+
landLayoutPage(createServerStudioCache(queryClient, {
|
|
1333
|
+
reader: requestReader,
|
|
1334
|
+
locale: located.locale
|
|
1335
|
+
}), shown, {
|
|
1336
|
+
projectUuid,
|
|
1337
|
+
options,
|
|
1338
|
+
pathname
|
|
1339
|
+
}, { in: shown.draftId ?? null });
|
|
1340
|
+
page = layoutPageShown({
|
|
1341
|
+
page: shown,
|
|
1342
|
+
apiUrl,
|
|
1343
|
+
href,
|
|
1344
|
+
pathname
|
|
1279
1345
|
});
|
|
1280
|
-
|
|
1281
|
-
const {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
<html${renderedHead.htmlAttrs}>
|
|
1312
|
-
<head>
|
|
1313
|
-
${renderStudioThemeScript(studio)}
|
|
1314
|
-
${renderedHead.headTags}
|
|
1315
|
-
${renderAnalyticsScripts(analytics)}
|
|
1316
|
-
${studio ? "" : renderBlockPreloads(options, dehydratedState)}
|
|
1317
|
-
<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(pageRenderInput, appHtml))}<\/script>
|
|
1318
|
-
</head>
|
|
1319
|
-
<body${renderedHead.bodyAttrs}>
|
|
1320
|
-
${renderedHead.bodyTagsOpen}
|
|
1321
|
-
<div id="root">${appHtml}</div>
|
|
1322
|
-
${renderedHead.bodyTags}
|
|
1323
|
-
${renderClientEntryScript(options.pageClientEntryUrl)}
|
|
1324
|
-
</body>
|
|
1325
|
-
</html>`, { headers });
|
|
1326
|
-
} catch (error) {
|
|
1327
|
-
if (isNotFoundError(error)) {
|
|
1328
|
-
const derived = await createDerivedResponse(options, pathname, located, request);
|
|
1329
|
-
if (derived) return derived;
|
|
1330
|
-
return new Response("No Camox page on this path", { status: 404 });
|
|
1331
|
-
}
|
|
1332
|
-
throw error;
|
|
1346
|
+
site = shown.blocks.project;
|
|
1347
|
+
const { blocks, record } = shown;
|
|
1348
|
+
readAlternatesOf = {
|
|
1349
|
+
identity: record && shown.layout._internal.kind === "derived" ? {
|
|
1350
|
+
kind: "record",
|
|
1351
|
+
layout: shown.layout,
|
|
1352
|
+
recordId: record.item.data.id
|
|
1353
|
+
} : { kind: "code-owned" },
|
|
1354
|
+
described: {
|
|
1355
|
+
project: {
|
|
1356
|
+
...blocks.project,
|
|
1357
|
+
name: blocks.projectName
|
|
1358
|
+
},
|
|
1359
|
+
title: shown.context.page.title,
|
|
1360
|
+
declared: declarePage({
|
|
1361
|
+
layout: shown.layout._internal,
|
|
1362
|
+
context: shown.context
|
|
1363
|
+
}),
|
|
1364
|
+
blocks: placedBlockNodes(blocks.layout, [], blocks.structuredData),
|
|
1365
|
+
...record?.item.published && { published: record.item.published },
|
|
1366
|
+
records: pageRecords(blocks.recordStructuredData, record && {
|
|
1367
|
+
id: record.item.data.id,
|
|
1368
|
+
structuredData: record.item.structuredData,
|
|
1369
|
+
titles: record.values.titles
|
|
1370
|
+
})
|
|
1371
|
+
},
|
|
1372
|
+
localeReads: requestLocaleReads(options, {
|
|
1373
|
+
reader: requestReader,
|
|
1374
|
+
draftId: shown.draftId
|
|
1375
|
+
})
|
|
1376
|
+
};
|
|
1333
1377
|
}
|
|
1378
|
+
const { alternates, head } = await readAlternates(request, options, {
|
|
1379
|
+
...readAlternatesOf,
|
|
1380
|
+
located,
|
|
1381
|
+
head: withSearchEngineMark(page.head, site)
|
|
1382
|
+
});
|
|
1383
|
+
const studio = requestReader.member !== void 0;
|
|
1384
|
+
const analytics = dataOnly ? void 0 : visitorAnalytics({
|
|
1385
|
+
studio,
|
|
1386
|
+
draftId: shown.draftId
|
|
1387
|
+
}, site);
|
|
1388
|
+
const dehydratedState = dehydrate(queryClient);
|
|
1389
|
+
const input = await withArtifactsByUrl({
|
|
1390
|
+
...page,
|
|
1391
|
+
head,
|
|
1392
|
+
alternates,
|
|
1393
|
+
project: requestReader.member,
|
|
1394
|
+
presentation: studio ? "studio" : "public",
|
|
1395
|
+
...analytics && { analytics },
|
|
1396
|
+
apiUrl,
|
|
1397
|
+
authenticationUrl: options.authenticationUrl ?? "",
|
|
1398
|
+
projectUuid,
|
|
1399
|
+
dehydratedState,
|
|
1400
|
+
draftId: shown.draftId,
|
|
1401
|
+
refusedDraft: shown.refusedDraft,
|
|
1402
|
+
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
1403
|
+
locale: located.locale,
|
|
1404
|
+
locales: located.locales
|
|
1405
|
+
}, options);
|
|
1406
|
+
const document = await options.getDocument?.() ?? {};
|
|
1407
|
+
preparePreviewDocument(input, options, document);
|
|
1408
|
+
const headers = readerHeaders(requestReader);
|
|
1409
|
+
if (dataOnly) return Response.json(input, { headers: {
|
|
1410
|
+
...headers,
|
|
1411
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
1412
|
+
} });
|
|
1413
|
+
const appHtml = await renderPage(input);
|
|
1414
|
+
return htmlResponse(htmlDocument({
|
|
1415
|
+
head: renderHead(studio ? {} : document, createPageHeadInput(head), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale),
|
|
1416
|
+
before: renderStudioThemeScript(studio),
|
|
1417
|
+
after: `${renderAnalyticsScripts(analytics)}${studio ? "" : renderBlockPreloads(options, dehydratedState)}`,
|
|
1418
|
+
data: hydrationInputFor(input, appHtml),
|
|
1419
|
+
root: appHtml,
|
|
1420
|
+
entryUrl: options.pageClientEntryUrl
|
|
1421
|
+
}), {
|
|
1422
|
+
...headers,
|
|
1423
|
+
Link: llmsTxtLinkHeader(options),
|
|
1424
|
+
...isHiddenFromSearchEngines(site) && { "X-Robots-Tag": "noindex" }
|
|
1425
|
+
});
|
|
1334
1426
|
}
|
|
1335
1427
|
/**
|
|
1336
|
-
* The context `loader` and `routes` get. Its record lookup reads each record's current content
|
|
1337
|
-
*
|
|
1428
|
+
* The context `loader` and `routes` get. Its record lookup reads each record's current content as
|
|
1429
|
+
* the request's Reader, in the draft the request settled on.
|
|
1338
1430
|
*/
|
|
1339
|
-
function createLayoutRequestContext(options,
|
|
1340
|
-
return createRequestContext({ records: createRecordLookup(options,
|
|
1431
|
+
function createLayoutRequestContext(options, reader, scope) {
|
|
1432
|
+
return createRequestContext({ records: createRecordLookup(options, reader, scope) });
|
|
1341
1433
|
}
|
|
1342
1434
|
/**
|
|
1343
|
-
* Reads the records `loader` and `routes` ask for, in `locale` or the
|
|
1344
|
-
* without an API.
|
|
1435
|
+
* Reads the records `loader` and `routes` ask for as the request's Reader, in `locale` or the
|
|
1436
|
+
* default locale; none without an API.
|
|
1345
1437
|
*/
|
|
1346
|
-
function createRecordLookup(options,
|
|
1438
|
+
function createRecordLookup(options, reader, scope = {}) {
|
|
1347
1439
|
const { apiUrl, projectUuid } = options;
|
|
1348
1440
|
if (!apiUrl || !projectUuid) return void 0;
|
|
1349
|
-
const api =
|
|
1350
|
-
authCookieHeader: source === "draft" ? authCookieHeader : void 0,
|
|
1351
|
-
lookToken: source === "draft" ? lookToken : void 0,
|
|
1352
|
-
draftId,
|
|
1353
|
-
locale
|
|
1354
|
-
});
|
|
1441
|
+
const api = reader.api(scope);
|
|
1355
1442
|
return {
|
|
1356
1443
|
findByField: (input) => api.collectionDefinitions.findRecordsByField({
|
|
1357
1444
|
...input,
|
|
@@ -1363,155 +1450,116 @@ function createRecordLookup(options, source, { authCookieHeader, lookToken, draf
|
|
|
1363
1450
|
})
|
|
1364
1451
|
};
|
|
1365
1452
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
return typeof result.collectionId === "string" && !!app?.getCollectionById(result.collectionId) && typeof data?.id === "string" && RECORD_ID.test(data.id) && typeof data.content === "object" && data.content !== null;
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Runs a code-owned page's loader in the page's locale. The envelope survives SSR, navigation and
|
|
1374
|
-
* hydration, and its data renders as returned: Camox never translates it.
|
|
1375
|
-
*/
|
|
1376
|
-
async function loadLayoutResult(app, match, context, locale) {
|
|
1377
|
-
const { loader, id } = match.layout._internal;
|
|
1378
|
-
if (!loader) return void 0;
|
|
1379
|
-
const result = await loader({
|
|
1380
|
-
params: match.params,
|
|
1381
|
-
context,
|
|
1382
|
-
locale: locale.tag
|
|
1383
|
-
});
|
|
1384
|
-
const serializedResult = result === void 0 ? void 0 : JSON.parse(JSON.stringify(result));
|
|
1385
|
-
const record = asCollectionItem(serializedResult);
|
|
1386
|
-
if (!(record ? isValidCollectionItem(app, record) : serializedResult?.kind === "data" && Object.hasOwn(serializedResult, "data"))) throw new Error(`Layout "${id}" loader must return { kind: "data", data } with JSON-serializable data, or a collection-item envelope naming a registered collection and a record id`);
|
|
1387
|
-
return serializedResult;
|
|
1453
|
+
/** Whether an API error refuses the Draft a read named, rather than finding nothing in it. */
|
|
1454
|
+
function refusesDraft(error, draftId) {
|
|
1455
|
+
const data = error?.data;
|
|
1456
|
+
return draftId !== void 0 && data?.draftId === draftId;
|
|
1388
1457
|
}
|
|
1389
1458
|
/**
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1459
|
+
* What a page's Markdown is, at a path: whatever Page Read finds there, read as `reader` reads
|
|
1460
|
+
* it. A curated page's comes from `curated`, a Layout Page's from its layout's blocks and its
|
|
1461
|
+
* `toMarkdown`. Null when the page has none: it's then HTML only.
|
|
1393
1462
|
*/
|
|
1394
|
-
async function
|
|
1395
|
-
const {
|
|
1396
|
-
const
|
|
1397
|
-
if (!toMarkdown || !apiUrl || !projectUuid) return null;
|
|
1398
|
-
const authCookieHeader = reader.member ? reader.authCookieHeader : void 0;
|
|
1399
|
-
const api = (draftId) => createServerApiClient(apiUrl, {
|
|
1400
|
-
authCookieHeader,
|
|
1463
|
+
async function readPageMarkdownShown(options, { app, path, page, reader, requestReader, curated }) {
|
|
1464
|
+
const { projectUuid } = options;
|
|
1465
|
+
const api = (draftId) => requestReader.api({
|
|
1401
1466
|
draftId,
|
|
1402
|
-
locale
|
|
1467
|
+
locale: reader.locale
|
|
1403
1468
|
});
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1469
|
+
const shown = await readPage({
|
|
1470
|
+
app,
|
|
1471
|
+
path,
|
|
1472
|
+
pathname: page.pathname,
|
|
1473
|
+
reader,
|
|
1474
|
+
curated: async () => ({ value: curated?.page ? curated : null }),
|
|
1475
|
+
layout: (draftId, layoutId) => api(draftId).blocks.getLayoutMarkdown({
|
|
1476
|
+
projectUuid,
|
|
1477
|
+
layoutId
|
|
1478
|
+
}).catch((error) => {
|
|
1479
|
+
if (!isNotFoundError(error) || refusesDraft(error, draftId)) throw error;
|
|
1480
|
+
return {
|
|
1411
1481
|
before: "",
|
|
1412
1482
|
after: "",
|
|
1413
1483
|
projectName: ""
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
};
|
|
1484
|
+
};
|
|
1485
|
+
})
|
|
1417
1486
|
});
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1487
|
+
if (shown.kind === "none") {
|
|
1488
|
+
if (shown.layout) throw notFound();
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
if (shown.kind === "curated") return {
|
|
1492
|
+
kind: "curated",
|
|
1493
|
+
read: shown.page
|
|
1494
|
+
};
|
|
1495
|
+
const layout = shown.layout._internal;
|
|
1496
|
+
const { toMarkdown } = layout;
|
|
1497
|
+
if (!toMarkdown) return {
|
|
1498
|
+
kind: "layout",
|
|
1499
|
+
markdown: null
|
|
1500
|
+
};
|
|
1501
|
+
const { record, params, blocks } = shown;
|
|
1502
|
+
const data = shown.result?.data;
|
|
1503
|
+
const body = record ? [await api(shown.draftId).collectionDefinitions.recordMarkdown({
|
|
1504
|
+
projectUuid,
|
|
1505
|
+
collectionId: record.item.collectionId,
|
|
1506
|
+
id: record.item.data.id,
|
|
1507
|
+
toMarkdown: recordMarkdownTemplate(record.collection, (item) => toMarkdown({
|
|
1437
1508
|
data,
|
|
1438
|
-
params
|
|
1509
|
+
params,
|
|
1439
1510
|
item
|
|
1440
|
-
}))
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
id: data.id,
|
|
1446
|
-
toMarkdown: template
|
|
1447
|
-
})],
|
|
1448
|
-
data,
|
|
1449
|
-
record: recordPageValues(collection, record)
|
|
1450
|
-
};
|
|
1451
|
-
};
|
|
1452
|
-
const rendered = await layoutMarkdown();
|
|
1511
|
+
}))
|
|
1512
|
+
})] : toMarkdown({
|
|
1513
|
+
data,
|
|
1514
|
+
params
|
|
1515
|
+
});
|
|
1453
1516
|
const meta = buildPageMeta({
|
|
1454
|
-
layout
|
|
1455
|
-
context:
|
|
1456
|
-
path: page.pathname,
|
|
1457
|
-
locale: locale.tag,
|
|
1458
|
-
projectName: slots.projectName,
|
|
1459
|
-
data: rendered.data,
|
|
1460
|
-
params: match.params,
|
|
1461
|
-
record: "record" in rendered ? rendered.record : void 0
|
|
1462
|
-
})
|
|
1517
|
+
layout,
|
|
1518
|
+
context: shown.context
|
|
1463
1519
|
});
|
|
1464
1520
|
const markdown = [
|
|
1465
|
-
|
|
1466
|
-
...
|
|
1467
|
-
|
|
1521
|
+
blocks.before,
|
|
1522
|
+
...body,
|
|
1523
|
+
blocks.after
|
|
1468
1524
|
].filter(Boolean).join("\n\n");
|
|
1469
|
-
return
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
* reads published records and blocks whoever asks.
|
|
1477
|
-
*/
|
|
1478
|
-
async function createLayoutMarkdownResponse(options, match, locale, page) {
|
|
1479
|
-
let markdown;
|
|
1480
|
-
try {
|
|
1481
|
-
markdown = await buildLayoutMarkdown(options, match, locale, page);
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
if (isNotFoundError(error)) return new Response("Page not found", { status: 404 });
|
|
1484
|
-
throw error;
|
|
1485
|
-
}
|
|
1486
|
-
if (markdown === null) return null;
|
|
1487
|
-
return new Response(markdown, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
|
|
1525
|
+
return {
|
|
1526
|
+
kind: "layout",
|
|
1527
|
+
markdown: withFrontMatter(markdown, {
|
|
1528
|
+
...meta,
|
|
1529
|
+
url: page.url
|
|
1530
|
+
})
|
|
1531
|
+
};
|
|
1488
1532
|
}
|
|
1489
1533
|
/**
|
|
1490
|
-
* A
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1534
|
+
* A Layout Page's Markdown Representation as JSON (`/_camox/data?markdown`), read like the page's
|
|
1535
|
+
* data: in the draft the URL names, for a member. The studio shows it, while the page's `.md` URL
|
|
1536
|
+
* always reads Live. `markdown` is null when the layout declares none. A curated page's Markdown
|
|
1537
|
+
* is the API's to read.
|
|
1493
1538
|
*/
|
|
1494
1539
|
async function createLayoutMarkdownDataResponse(options, request, pathname) {
|
|
1495
|
-
const
|
|
1496
|
-
const
|
|
1497
|
-
const
|
|
1498
|
-
if (!
|
|
1499
|
-
const
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
Vary: "Cookie",
|
|
1503
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
|
|
1504
|
-
};
|
|
1540
|
+
const { apiUrl, projectUuid } = options;
|
|
1541
|
+
const requestReader = await resolveReader(request, options);
|
|
1542
|
+
const located = await locateRequest(requestReader, options, pathname);
|
|
1543
|
+
if (!apiUrl || !projectUuid || matchLocalizedRoute(located.path)?.kind !== "page") return new Response("Not found", { status: 404 });
|
|
1544
|
+
const headers = readerHeaders(requestReader);
|
|
1545
|
+
const reader = pageReaderOf(options, requestReader, located.locale);
|
|
1546
|
+
let shown;
|
|
1505
1547
|
try {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1548
|
+
shown = await readPageMarkdownShown({
|
|
1549
|
+
...options,
|
|
1550
|
+
apiUrl,
|
|
1551
|
+
projectUuid
|
|
1509
1552
|
}, {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1553
|
+
app: await options.getCamoxApp?.(),
|
|
1554
|
+
path: located.path,
|
|
1555
|
+
page: {
|
|
1556
|
+
pathname,
|
|
1557
|
+
url: absoluteUrl(request, options)(pathname)
|
|
1558
|
+
},
|
|
1559
|
+
reader,
|
|
1560
|
+
requestReader,
|
|
1561
|
+
curated: null
|
|
1513
1562
|
});
|
|
1514
|
-
return Response.json({ markdown }, { headers });
|
|
1515
1563
|
} catch (error) {
|
|
1516
1564
|
if (isNotFoundError(error)) return new Response("Not found", {
|
|
1517
1565
|
status: 404,
|
|
@@ -1519,160 +1567,16 @@ async function createLayoutMarkdownDataResponse(options, request, pathname) {
|
|
|
1519
1567
|
});
|
|
1520
1568
|
throw error;
|
|
1521
1569
|
}
|
|
1570
|
+
if (shown?.kind !== "layout") return new Response("Not found", { status: 404 });
|
|
1571
|
+
return Response.json({ markdown: shown.markdown }, { headers });
|
|
1522
1572
|
}
|
|
1523
|
-
async function
|
|
1524
|
-
const app = await options.getCamoxApp?.();
|
|
1525
|
-
const match = app && matchDerivedLayout(app.getLayouts(), located.path);
|
|
1526
|
-
if (!match || !dataOnly && !options.renderPage) return null;
|
|
1527
|
-
if (singletonOnly && match.layout._internal.kind !== "singleton") return null;
|
|
1528
|
-
try {
|
|
1529
|
-
const queryClient = new QueryClient();
|
|
1530
|
-
const { project, clearAuthCookie } = await resolveRequestMember(request, options);
|
|
1531
|
-
const studio = project !== void 0;
|
|
1532
|
-
const authCookieHeader = studio ? getServerAuthCookieHeader(request.headers) : void 0;
|
|
1533
|
-
const requestedDraft = getRequestDraftId(request);
|
|
1534
|
-
const readLayout = () => readDraftOrLive(requestedDraft, studio, (draftId) => createServerApiClient(options.apiUrl, {
|
|
1535
|
-
authCookieHeader,
|
|
1536
|
-
draftId,
|
|
1537
|
-
locale: located.locale
|
|
1538
|
-
}).layouts.get({
|
|
1539
|
-
projectUuid: options.projectUuid,
|
|
1540
|
-
layoutId: match.layout._internal.id
|
|
1541
|
-
}));
|
|
1542
|
-
const runLoader = (draftId) => loadLayoutResult(app, match, createLayoutRequestContext(options, studio ? "draft" : "live", {
|
|
1543
|
-
authCookieHeader,
|
|
1544
|
-
draftId,
|
|
1545
|
-
locale: located.locale
|
|
1546
|
-
}), located.locale);
|
|
1547
|
-
const [serializedResult, { value: shared, ...read }] = studio && requestedDraft ? await readLayout().then(async (read) => [await runLoader(read.draftId), read]) : await Promise.all([runLoader(), readLayout()]);
|
|
1548
|
-
const studioCache = createServerStudioCache(queryClient, {
|
|
1549
|
-
apiUrl: options.apiUrl,
|
|
1550
|
-
authCookieHeader,
|
|
1551
|
-
locale: located.locale
|
|
1552
|
-
});
|
|
1553
|
-
const projectUuid = options.projectUuid;
|
|
1554
|
-
const shownIn = { in: read.draftId ?? null };
|
|
1555
|
-
studioCache.land(reads.layoutStructure({
|
|
1556
|
-
projectUuid,
|
|
1557
|
-
layoutId: match.layout._internal.id
|
|
1558
|
-
}), shared, shownIn);
|
|
1559
|
-
studioCache.set(reads.derivedPageData({
|
|
1560
|
-
projectUuid,
|
|
1561
|
-
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
1562
|
-
pathname
|
|
1563
|
-
}), serializedResult ?? null, shownIn);
|
|
1564
|
-
const record = asCollectionItem(serializedResult);
|
|
1565
|
-
const recordCollection = record && app.getCollectionById(record.collectionId);
|
|
1566
|
-
const recordValues = record && recordCollection ? recordPageValues(recordCollection, record) : void 0;
|
|
1567
|
-
const context = buildLayoutContext(match.layout._internal, {
|
|
1568
|
-
path: pathname,
|
|
1569
|
-
locale: located.locale.tag,
|
|
1570
|
-
projectName: shared.projectName,
|
|
1571
|
-
data: serializedResult?.data,
|
|
1572
|
-
params: match.params,
|
|
1573
|
-
record: recordValues
|
|
1574
|
-
});
|
|
1575
|
-
const { alternates, head: pageHead } = await readAlternates(request, options, {
|
|
1576
|
-
located,
|
|
1577
|
-
identity: record && match.layout._internal.kind === "derived" ? {
|
|
1578
|
-
kind: "record",
|
|
1579
|
-
layout: match.layout,
|
|
1580
|
-
recordId: record.data.id
|
|
1581
|
-
} : { kind: "code-owned" },
|
|
1582
|
-
head: withSearchEngineMark(buildPageHead({
|
|
1583
|
-
layout: match.layout._internal,
|
|
1584
|
-
context,
|
|
1585
|
-
origin: new URL(request.url).origin,
|
|
1586
|
-
faviconUrl: buildFaviconUrl(options.apiUrl, shared.project),
|
|
1587
|
-
hasMarkdown: match.layout._internal.toMarkdown !== void 0,
|
|
1588
|
-
recordType: record?.structuredData?.node?.["@type"]
|
|
1589
|
-
}), shared.project),
|
|
1590
|
-
described: {
|
|
1591
|
-
project: {
|
|
1592
|
-
...shared.project,
|
|
1593
|
-
name: shared.projectName
|
|
1594
|
-
},
|
|
1595
|
-
title: context.page.title,
|
|
1596
|
-
declared: declarePage({
|
|
1597
|
-
layout: match.layout._internal,
|
|
1598
|
-
context
|
|
1599
|
-
}),
|
|
1600
|
-
blocks: placedBlockNodes(shared.layout, [], shared.structuredData),
|
|
1601
|
-
...record?.published && { published: record.published },
|
|
1602
|
-
records: pageRecords(shared.recordStructuredData, record && {
|
|
1603
|
-
id: record.data.id,
|
|
1604
|
-
structuredData: record.structuredData,
|
|
1605
|
-
titles: recordValues?.titles
|
|
1606
|
-
})
|
|
1607
|
-
},
|
|
1608
|
-
localeReads: requestLocaleReads(options, {
|
|
1609
|
-
authCookieHeader,
|
|
1610
|
-
draftId: read.draftId
|
|
1611
|
-
})
|
|
1612
|
-
});
|
|
1613
|
-
const analytics = dataOnly ? void 0 : visitorAnalytics({
|
|
1614
|
-
studio,
|
|
1615
|
-
draftId: read.draftId
|
|
1616
|
-
}, shared.project);
|
|
1617
|
-
const input = await withArtifactsByUrl({
|
|
1618
|
-
alternates,
|
|
1619
|
-
project,
|
|
1620
|
-
presentation: studio ? "studio" : "public",
|
|
1621
|
-
...analytics && { analytics },
|
|
1622
|
-
apiUrl: options.apiUrl,
|
|
1623
|
-
authenticationUrl: options.authenticationUrl,
|
|
1624
|
-
projectUuid: options.projectUuid,
|
|
1625
|
-
dehydratedState: dehydrate(queryClient),
|
|
1626
|
-
draftId: read.draftId,
|
|
1627
|
-
refusedDraft: read.refusedDraft,
|
|
1628
|
-
head: pageHead,
|
|
1629
|
-
href: renderedHref(request.url, read),
|
|
1630
|
-
layoutIdentity: getLayoutIdentity(shared.layout),
|
|
1631
|
-
loaderData: null,
|
|
1632
|
-
pathname,
|
|
1633
|
-
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
1634
|
-
locale: located.locale,
|
|
1635
|
-
locales: located.locales,
|
|
1636
|
-
derived: {
|
|
1637
|
-
layoutId: match.layout._internal.id,
|
|
1638
|
-
page: {
|
|
1639
|
-
params: match.params,
|
|
1640
|
-
path: pathname,
|
|
1641
|
-
projectName: shared.projectName
|
|
1642
|
-
},
|
|
1643
|
-
result: serializedResult,
|
|
1644
|
-
layout: shared.layout
|
|
1645
|
-
}
|
|
1646
|
-
}, options);
|
|
1647
|
-
const document = await options.getDocument?.() ?? {};
|
|
1648
|
-
preparePreviewDocument(input, options, document);
|
|
1649
|
-
if (dataOnly) return Response.json(input, { headers: {
|
|
1650
|
-
...isPrivateRender(request) ? { "Cache-Control": "private, no-store" } : {},
|
|
1651
|
-
Vary: "Cookie",
|
|
1652
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
|
|
1653
|
-
} });
|
|
1654
|
-
const html = await options.renderPage(input);
|
|
1655
|
-
const head = renderHead(studio ? {} : document, createPageHeadInput(pageHead), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale);
|
|
1656
|
-
return new Response(`<!doctype html><html${head.htmlAttrs}><head>${renderStudioThemeScript(studio)}${head.headTags}${renderAnalyticsScripts(analytics)}${studio ? "" : renderBlockPreloads(options, input.dehydratedState)}<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(input, html))}<\/script></head><body${head.bodyAttrs}>${head.bodyTagsOpen}<div id="root">${html}</div>${head.bodyTags}${renderClientEntryScript(options.pageClientEntryUrl)}</body></html>`, { headers: {
|
|
1657
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1658
|
-
Vary: "Cookie",
|
|
1659
|
-
Link: llmsTxtLinkHeader(options),
|
|
1660
|
-
...isPrivateRender(request) ? { "Cache-Control": "private, no-store" } : {},
|
|
1661
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {},
|
|
1662
|
-
...isHiddenFromSearchEngines(shared.project) && { "X-Robots-Tag": "noindex" }
|
|
1663
|
-
} });
|
|
1664
|
-
} catch (error) {
|
|
1665
|
-
if (isNotFoundError(error)) return new Response("Page not found", { status: 404 });
|
|
1666
|
-
throw error;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
async function createStudioHtmlResponse({ match, options, pathname, located, request, dataOnly = false }) {
|
|
1573
|
+
async function createStudioHtmlResponse({ match, options, pathname, located, request, reader, dataOnly = false }) {
|
|
1670
1574
|
if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !dataOnly && !options.renderStudio) return Response.json({
|
|
1671
1575
|
message: "Camox Studio renderer is not configured.",
|
|
1672
1576
|
...match
|
|
1673
1577
|
}, { status: 500 });
|
|
1674
1578
|
const queryClient = new QueryClient();
|
|
1675
|
-
const
|
|
1579
|
+
const project = reader.member;
|
|
1676
1580
|
const presentation = project ? "studio" : "public";
|
|
1677
1581
|
const studioRenderInput = {
|
|
1678
1582
|
project,
|
|
@@ -1690,46 +1594,30 @@ async function createStudioHtmlResponse({ match, options, pathname, located, req
|
|
|
1690
1594
|
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
1691
1595
|
locale: located.locale,
|
|
1692
1596
|
locales: located.locales,
|
|
1693
|
-
draftId:
|
|
1597
|
+
draftId: reader.draftId
|
|
1694
1598
|
};
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
Vary: "Cookie",
|
|
1698
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
|
|
1699
|
-
} });
|
|
1599
|
+
const headers = readerHeaders(reader, { alwaysPrivate: true });
|
|
1600
|
+
if (dataOnly) return Response.json(studioRenderInput, { headers });
|
|
1700
1601
|
const appHtml = await options.renderStudio(studioRenderInput);
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
</head>
|
|
1709
|
-
<body${renderedHead.bodyAttrs}>
|
|
1710
|
-
${renderedHead.bodyTagsOpen}
|
|
1711
|
-
<div id="root">${appHtml}</div>
|
|
1712
|
-
${renderedHead.bodyTags}
|
|
1713
|
-
${renderClientEntryScript(options.studioClientEntryUrl)}
|
|
1714
|
-
</body>
|
|
1715
|
-
</html>`, { headers: {
|
|
1716
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1717
|
-
"Cache-Control": "private, no-store",
|
|
1718
|
-
Vary: "Cookie",
|
|
1719
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
|
|
1720
|
-
} });
|
|
1602
|
+
return htmlResponse(htmlDocument({
|
|
1603
|
+
head: renderHead({}, { title: "Camox Studio" }, void 0, presentation === "studio" ? options.studioStylesheetUrl : void 0),
|
|
1604
|
+
before: renderStudioThemeScript(presentation === "studio"),
|
|
1605
|
+
data: studioRenderInput,
|
|
1606
|
+
root: appHtml,
|
|
1607
|
+
entryUrl: options.studioClientEntryUrl
|
|
1608
|
+
}), headers);
|
|
1721
1609
|
}
|
|
1722
1610
|
/**
|
|
1723
1611
|
* The Canvas with nothing selected renders the page app, like a Page, so
|
|
1724
1612
|
* selecting it keeps the Canvas mounted.
|
|
1725
1613
|
*/
|
|
1726
|
-
async function createCanvasOnlyResponse({ options, pathname, located, request, dataOnly = false }) {
|
|
1614
|
+
async function createCanvasOnlyResponse({ options, pathname, located, request, reader, dataOnly = false }) {
|
|
1727
1615
|
if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !dataOnly && !options.renderPage) return Response.json({
|
|
1728
1616
|
message: "Camox page renderer is not configured.",
|
|
1729
1617
|
kind: "canvas",
|
|
1730
1618
|
pathname
|
|
1731
1619
|
}, { status: 500 });
|
|
1732
|
-
const
|
|
1620
|
+
const project = reader.member;
|
|
1733
1621
|
const studio = project !== void 0;
|
|
1734
1622
|
const title = "Canvas";
|
|
1735
1623
|
const input = {
|
|
@@ -1748,87 +1636,18 @@ async function createCanvasOnlyResponse({ options, pathname, located, request, d
|
|
|
1748
1636
|
loaderData: null,
|
|
1749
1637
|
dehydratedState: dehydrate(new QueryClient()),
|
|
1750
1638
|
canvasOnly: true,
|
|
1751
|
-
draftId:
|
|
1752
|
-
};
|
|
1753
|
-
const headers = {
|
|
1754
|
-
"Cache-Control": "private, no-store",
|
|
1755
|
-
Vary: "Cookie",
|
|
1756
|
-
...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
|
|
1639
|
+
draftId: reader.draftId
|
|
1757
1640
|
};
|
|
1641
|
+
const headers = readerHeaders(reader, { alwaysPrivate: true });
|
|
1758
1642
|
if (dataOnly) return Response.json(input, { headers });
|
|
1759
1643
|
const html = await options.renderPage(input);
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
if (!options.apiUrl || !options.projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
|
|
1768
|
-
const singleton = await createDerivedResponse(options, pathname, located, request, {
|
|
1769
|
-
dataOnly: true,
|
|
1770
|
-
singletonOnly: true
|
|
1771
|
-
});
|
|
1772
|
-
if (singleton) return singleton;
|
|
1773
|
-
const { queryClient, result: reading } = pageRead ?? readCuratedPage({
|
|
1774
|
-
...options,
|
|
1775
|
-
apiUrl: options.apiUrl,
|
|
1776
|
-
projectUuid: options.projectUuid
|
|
1777
|
-
}, request, located);
|
|
1778
|
-
const localeReads = curatedPageLocaleReads(options, request, located, queryClient);
|
|
1779
|
-
try {
|
|
1780
|
-
const result = await reading;
|
|
1781
|
-
const headers = new Headers({
|
|
1782
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
1783
|
-
Vary: "Cookie"
|
|
1784
|
-
});
|
|
1785
|
-
if (isPrivateRender(request)) headers.set("Cache-Control", "private, no-store");
|
|
1786
|
-
if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
|
|
1787
|
-
const camoxApp = await options.getCamoxApp?.();
|
|
1788
|
-
const { alternates, head } = await readAlternates(request, options, {
|
|
1789
|
-
located,
|
|
1790
|
-
identity: {
|
|
1791
|
-
kind: "curated",
|
|
1792
|
-
pageId: result.data.page.page.id
|
|
1793
|
-
},
|
|
1794
|
-
head: withSearchEngineMark(camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null, result.data.page.project),
|
|
1795
|
-
described: curatedPageDescription(camoxApp, result.data.page, located.locale),
|
|
1796
|
-
localeReads: localeReads(result.draftId)
|
|
1797
|
-
});
|
|
1798
|
-
const dehydratedState = dehydrate(queryClient);
|
|
1799
|
-
const navigationInput = await withArtifactsByUrl({
|
|
1800
|
-
alternates,
|
|
1801
|
-
apiUrl: options.apiUrl,
|
|
1802
|
-
authenticationUrl: options.authenticationUrl ?? "",
|
|
1803
|
-
projectUuid: options.projectUuid,
|
|
1804
|
-
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
1805
|
-
locale: located.locale,
|
|
1806
|
-
locales: located.locales,
|
|
1807
|
-
href: renderedHref(request.url, result),
|
|
1808
|
-
draftId: result.draftId,
|
|
1809
|
-
refusedDraft: result.refusedDraft,
|
|
1810
|
-
presentation: result.project ? "studio" : "public",
|
|
1811
|
-
pathname,
|
|
1812
|
-
layoutIdentity: getLayoutIdentity(result.data.page.layout),
|
|
1813
|
-
project: result.project,
|
|
1814
|
-
loaderData: result.data,
|
|
1815
|
-
head,
|
|
1816
|
-
dehydratedState
|
|
1817
|
-
}, options);
|
|
1818
|
-
preparePreviewDocument(navigationInput, options, await options.getDocument?.() ?? {});
|
|
1819
|
-
return Response.json(navigationInput, { headers });
|
|
1820
|
-
} catch (error) {
|
|
1821
|
-
if (isNotFoundError(error)) {
|
|
1822
|
-
const derived = await createDerivedResponse(options, pathname, located, request, { dataOnly: true });
|
|
1823
|
-
if (derived) return derived;
|
|
1824
|
-
return Response.json({
|
|
1825
|
-
kind: "page",
|
|
1826
|
-
message: "No Camox page on this path",
|
|
1827
|
-
pathname
|
|
1828
|
-
}, { status: 404 });
|
|
1829
|
-
}
|
|
1830
|
-
throw error;
|
|
1831
|
-
}
|
|
1644
|
+
return htmlResponse(htmlDocument({
|
|
1645
|
+
head: renderHead({}, { title }, void 0, studio ? options.studioStylesheetUrl : void 0),
|
|
1646
|
+
before: renderStudioThemeScript(studio),
|
|
1647
|
+
data: input,
|
|
1648
|
+
root: html,
|
|
1649
|
+
entryUrl: options.pageClientEntryUrl
|
|
1650
|
+
}), headers);
|
|
1832
1651
|
}
|
|
1833
1652
|
async function handleCamoxRequest(request, options = {}) {
|
|
1834
1653
|
const pathname = getRuntimePathname(request.url, options.runtimeBasePath);
|
|
@@ -1889,14 +1708,16 @@ async function handleCamoxRequest(request, options = {}) {
|
|
|
1889
1708
|
* With `dataOnly`, as the JSON client navigation loads.
|
|
1890
1709
|
*/
|
|
1891
1710
|
async function respondInLocale(options, request, pathname, { dataOnly = false } = {}) {
|
|
1892
|
-
const
|
|
1711
|
+
const reader = await resolveReader(request, options);
|
|
1712
|
+
const [early, located] = await Promise.all([readPageEarly(options, reader, pathname), locateRequest(reader, options, pathname)]);
|
|
1893
1713
|
const localized = matchLocalizedRoute(located.path);
|
|
1894
1714
|
if (!localized) return new Response("Not found", { status: 404 });
|
|
1895
1715
|
const target = {
|
|
1896
1716
|
options,
|
|
1897
1717
|
pathname,
|
|
1898
1718
|
located,
|
|
1899
|
-
request
|
|
1719
|
+
request,
|
|
1720
|
+
reader
|
|
1900
1721
|
};
|
|
1901
1722
|
if (localized.kind === "canvas") return createCanvasOnlyResponse({
|
|
1902
1723
|
...target,
|
|
@@ -1907,11 +1728,11 @@ async function respondInLocale(options, request, pathname, { dataOnly = false }
|
|
|
1907
1728
|
match: localized,
|
|
1908
1729
|
dataOnly
|
|
1909
1730
|
});
|
|
1910
|
-
|
|
1731
|
+
return createPageResponse({
|
|
1911
1732
|
...target,
|
|
1912
|
-
pageRead:
|
|
1913
|
-
|
|
1914
|
-
|
|
1733
|
+
pageRead: earlyReadIn(early, located),
|
|
1734
|
+
dataOnly
|
|
1735
|
+
});
|
|
1915
1736
|
}
|
|
1916
1737
|
/**
|
|
1917
1738
|
* A page's Markdown Representation in its locale, with the same precedence as its HTML:
|
|
@@ -1923,6 +1744,7 @@ async function createPageMarkdownResponse(options, { pathname, request, pagesOnl
|
|
|
1923
1744
|
try {
|
|
1924
1745
|
read = await readPageMarkdown({
|
|
1925
1746
|
...options,
|
|
1747
|
+
reader: visitorReader(options),
|
|
1926
1748
|
path: pathname
|
|
1927
1749
|
});
|
|
1928
1750
|
} catch {
|
|
@@ -1935,10 +1757,25 @@ async function createPageMarkdownResponse(options, { pathname, request, pagesOnl
|
|
|
1935
1757
|
url: absoluteUrl(request, options)(pathname)
|
|
1936
1758
|
};
|
|
1937
1759
|
const app = await options.getCamoxApp?.();
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1760
|
+
const requestReader = visitorReader(options);
|
|
1761
|
+
const reader = pageReaderOf(options, requestReader, locale);
|
|
1762
|
+
let shown;
|
|
1763
|
+
try {
|
|
1764
|
+
shown = await readPageMarkdownShown(options, {
|
|
1765
|
+
app,
|
|
1766
|
+
path,
|
|
1767
|
+
page,
|
|
1768
|
+
reader,
|
|
1769
|
+
requestReader,
|
|
1770
|
+
curated: read
|
|
1771
|
+
});
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
if (isNotFoundError(error)) return new Response("Page not found", { status: 404 });
|
|
1774
|
+
throw error;
|
|
1775
|
+
}
|
|
1776
|
+
if (shown?.kind === "curated") return curatedMarkdownResponse(app, shown.read, page.url);
|
|
1777
|
+
if (!shown?.markdown) return null;
|
|
1778
|
+
return new Response(shown.markdown, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
|
|
1942
1779
|
}
|
|
1943
1780
|
/** Whether a route is one of the studio's own, rather than a page or the Canvas. */
|
|
1944
1781
|
function isStudioRoute(match) {
|
|
@@ -1953,18 +1790,13 @@ function matchLocalizedRoute(path) {
|
|
|
1953
1790
|
return match.kind === "page" || match.kind === "canvas" || isStudioRoute(match) ? match : null;
|
|
1954
1791
|
}
|
|
1955
1792
|
/**
|
|
1956
|
-
* The locale a request's path names, among the locales of the scope
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
1793
|
+
* The locale a request's path names, among the locales of the scope its Reader reads: its
|
|
1794
|
+
* draft's, else Live's. Without an API there are none to read: every path is the default
|
|
1795
|
+
* locale's, which then says it can't render.
|
|
1959
1796
|
*/
|
|
1960
|
-
async function locateRequest(
|
|
1797
|
+
async function locateRequest(reader, options, pathname) {
|
|
1961
1798
|
const { apiUrl, projectUuid } = options;
|
|
1962
|
-
const locales = apiUrl && projectUuid ? await
|
|
1963
|
-
apiUrl,
|
|
1964
|
-
projectUuid,
|
|
1965
|
-
authCookieHeader: getServerAuthCookieHeader(request.headers),
|
|
1966
|
-
draftId: getRequestDraftId(request)
|
|
1967
|
-
}) : [UNNAMED_DEFAULT_LOCALE];
|
|
1799
|
+
const locales = apiUrl && projectUuid ? await reader.locales() : [UNNAMED_DEFAULT_LOCALE];
|
|
1968
1800
|
return {
|
|
1969
1801
|
...parseLocalePath(pathname, locales),
|
|
1970
1802
|
locales
|