camox 0.46.0 → 0.48.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 +54 -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 +21 -6
- package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
- package/dist/features/canvas/useCanvasCamera.js +16 -11
- 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 +176 -145
- package/dist/features/preview/components/AddBlockSheet.js +1 -0
- package/dist/features/preview/components/BlockActionsPopover.js +8 -6
- package/dist/features/preview/components/PageInfo.js +244 -264
- package/dist/features/preview/components/PageTree.js +63 -53
- package/dist/features/preview/components/PreviewToolbar.js +74 -85
- 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/provider/CamoxProvider.js +8 -16
- package/dist/features/provider/CoreCamoxProvider.js +1 -6
- package/dist/features/provider/signInShortcut.js +60 -0
- package/dist/features/routes/pageRuntime.d.ts +15 -1
- package/dist/features/routes/pageRuntime.js +35 -38
- package/dist/features/runtime/RuntimeChrome.js +2 -2
- package/dist/features/runtime/breadcrumbs.js +68 -0
- package/dist/features/runtime/livePages.js +3 -2
- package/dist/features/runtime/pageApp.js +11 -39
- package/dist/features/runtime/pageGraph.js +92 -0
- package/dist/features/runtime/projectLogo.js +110 -0
- package/dist/features/runtime/runtime.d.ts +11 -6
- package/dist/features/runtime/runtime.js +278 -55
- 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/studio/components/ChatSidebar.js +5 -5
- package/dist/features/studio/components/ProjectMenu.js +127 -100
- package/dist/features/studio/components/SocialProfilesDialog.js +342 -0
- package/dist/features/studio/components/ThreadMessages.js +2 -2
- package/dist/features/vite/blockBoilerplate.js +3 -2
- package/dist/features/vite/layoutGeneration.js +3 -2
- package/dist/features/vite/vite.js +1 -0
- package/dist/lib/queries.d.ts +4 -0
- package/dist/lib/queries.js +1 -0
- package/dist/lib/studio-reads.js +3 -1
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +5 -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/provider/LocalhostPreviewProvider.js +0 -158
|
@@ -5,18 +5,24 @@ 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
11
|
import { STUDIO_ASSETS_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, matchCollectionContentPath } from "../studio/routes.js";
|
|
11
12
|
import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
|
|
13
|
+
import { DefaultPageImage, renderPageImage } from "../og/pageImage.js";
|
|
12
14
|
import { discoverLayoutPages, matchDerivedLayout } from "../../core/derivedRoutes.js";
|
|
13
|
-
import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
|
|
15
|
+
import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, declarePage, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, 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";
|
|
@@ -151,15 +157,22 @@ function absoluteUrl(request, options) {
|
|
|
151
157
|
const { origin } = new URL(request.url);
|
|
152
158
|
return (path) => `${origin}${withRuntimeBasePath(path, options.runtimeBasePath)}`;
|
|
153
159
|
}
|
|
160
|
+
/** An ETag derived from `text`. */
|
|
161
|
+
async function etagOf(text) {
|
|
162
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
163
|
+
return `"${[...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
|
|
164
|
+
}
|
|
165
|
+
/** Whether the request's `If-None-Match` names `etag`, so a 304 answers it. */
|
|
166
|
+
function namesEtag(request, etag) {
|
|
167
|
+
return (request.headers.get("If-None-Match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === etag || tag === "*");
|
|
168
|
+
}
|
|
154
169
|
/** `body` with an ETag derived from it, or a 304 when the request's `If-None-Match` names it. */
|
|
155
170
|
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
171
|
const taggedHeaders = {
|
|
159
172
|
...headers,
|
|
160
|
-
ETag:
|
|
173
|
+
ETag: await etagOf(body)
|
|
161
174
|
};
|
|
162
|
-
if ((request
|
|
175
|
+
if (namesEtag(request, taggedHeaders.ETag)) return new Response(null, {
|
|
163
176
|
status: 304,
|
|
164
177
|
headers: taggedHeaders
|
|
165
178
|
});
|
|
@@ -250,21 +263,94 @@ async function createRoutesResponse(request, options) {
|
|
|
250
263
|
});
|
|
251
264
|
}
|
|
252
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* The image of the page `/og?path=` names, by its path with its locale's prefix: its layout's
|
|
268
|
+
* `toImage`, or Camox's default image, rendered with the page's context. Only real pages have
|
|
269
|
+
* one, read as the page itself would be, in the draft the URL names for a member.
|
|
270
|
+
*/
|
|
253
271
|
async function createOgResponse(request, options) {
|
|
254
272
|
const camoxApp = await options.getCamoxApp?.();
|
|
255
273
|
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
|
-
|
|
266
|
-
|
|
274
|
+
const source = await readPageImageSource(request, options, camoxApp);
|
|
275
|
+
if (!source) return new Response("Not found", { status: 404 });
|
|
276
|
+
const headers = {
|
|
277
|
+
ETag: await etagOf(JSON.stringify([source.layout?.id ?? null, source.context])),
|
|
278
|
+
"Cache-Control": isPrivateRender(request, options) ? "private, no-store" : "public, no-cache"
|
|
279
|
+
};
|
|
280
|
+
if (namesEtag(request, headers.ETag)) return new Response(null, {
|
|
281
|
+
status: 304,
|
|
282
|
+
headers
|
|
283
|
+
});
|
|
284
|
+
const toImage = source.layout?.toImage ?? DefaultPageImage;
|
|
285
|
+
const image = await renderPageImage(toImage(source.context));
|
|
286
|
+
for (const [name, value] of Object.entries(headers)) image.headers.set(name, value);
|
|
287
|
+
return image;
|
|
288
|
+
}
|
|
289
|
+
/** The layout and context of the page `/og` names, or null when it names no page. */
|
|
290
|
+
async function readPageImageSource(request, options, app) {
|
|
291
|
+
const { apiUrl, projectUuid } = options;
|
|
292
|
+
const path = new URL(request.url).searchParams.get("path");
|
|
293
|
+
if (!apiUrl || !projectUuid || !path?.startsWith("/")) return null;
|
|
294
|
+
const located = await locateRequest(request, options, path);
|
|
295
|
+
if (matchLocalizedRoute(located.path)?.kind !== "page") return null;
|
|
296
|
+
const { project } = await resolveRequestMember(request, options);
|
|
297
|
+
const member = project !== void 0;
|
|
298
|
+
const authCookieHeader = member ? getServerAuthCookieHeader(request.headers) : void 0;
|
|
299
|
+
const requestedDraft = getRequestDraftId(request, options);
|
|
300
|
+
const api = (draftId) => createServerApiClient(apiUrl, {
|
|
301
|
+
authCookieHeader,
|
|
302
|
+
draftId,
|
|
303
|
+
locale: located.locale
|
|
267
304
|
});
|
|
305
|
+
const match = matchDerivedLayout(app.getLayouts(), located.path);
|
|
306
|
+
/** A code-owned page: its loader runs in the draft the layout read settled on. */
|
|
307
|
+
const codeOwned = async (match) => {
|
|
308
|
+
const layout = match.layout._internal;
|
|
309
|
+
const { value: shared, draftId } = await readDraftOrLive(requestedDraft, member, (draftId) => api(draftId).layouts.get({
|
|
310
|
+
projectUuid,
|
|
311
|
+
layoutId: layout.id
|
|
312
|
+
}));
|
|
313
|
+
const result = await loadLayoutResult(app, match, createLayoutRequestContext(options, member ? "draft" : "live", {
|
|
314
|
+
authCookieHeader,
|
|
315
|
+
draftId,
|
|
316
|
+
locale: located.locale
|
|
317
|
+
}), located.locale);
|
|
318
|
+
const record = asCollectionItem(result);
|
|
319
|
+
const collection = record && app.getCollectionById(record.collectionId);
|
|
320
|
+
return {
|
|
321
|
+
layout,
|
|
322
|
+
context: buildLayoutContext(layout, {
|
|
323
|
+
path,
|
|
324
|
+
locale: located.locale.tag,
|
|
325
|
+
projectName: shared.projectName,
|
|
326
|
+
data: result?.data,
|
|
327
|
+
params: match.params,
|
|
328
|
+
record: record && collection ? recordPageValues(collection, record) : void 0
|
|
329
|
+
})
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
/** A curated page, or else a derived layout's page, as the page's HTML falls back. */
|
|
333
|
+
const curated = async () => {
|
|
334
|
+
try {
|
|
335
|
+
const { value: page } = await readDraftOrLive(requestedDraft, member, (draftId) => api(draftId).pages.getByPath({
|
|
336
|
+
path: located.path,
|
|
337
|
+
projectUuid
|
|
338
|
+
}));
|
|
339
|
+
return {
|
|
340
|
+
layout: page.layout ? app.getLayoutById(page.layout.layoutId)?._internal : void 0,
|
|
341
|
+
context: curatedLayoutContext(page, located.locale)
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (isNotFoundError(error) && match) return codeOwned(match);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
try {
|
|
349
|
+
return match?.layout._internal.kind === "singleton" ? await codeOwned(match) : await curated();
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (isNotFoundError(error)) return null;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
268
354
|
}
|
|
269
355
|
function createDefaultHeadInput(stylesheetUrl) {
|
|
270
356
|
return {
|
|
@@ -303,9 +389,9 @@ function createPageHeadInput(head) {
|
|
|
303
389
|
const pageLinks = [];
|
|
304
390
|
for (const entry of links) {
|
|
305
391
|
if (!entry.href) continue;
|
|
306
|
-
if (entry.rel === "icon") {
|
|
392
|
+
if (entry.rel === "icon" || entry.rel === "canonical") {
|
|
307
393
|
pageLinks.push({
|
|
308
|
-
rel:
|
|
394
|
+
rel: entry.rel,
|
|
309
395
|
href: entry.href,
|
|
310
396
|
"data-camox-page-head": "true"
|
|
311
397
|
});
|
|
@@ -319,10 +405,16 @@ function createPageHeadInput(head) {
|
|
|
319
405
|
"data-camox-page-head": "true"
|
|
320
406
|
});
|
|
321
407
|
}
|
|
408
|
+
const pageScripts = (head.scripts ?? []).filter((entry) => entry.type === "application/ld+json").map((entry) => ({
|
|
409
|
+
type: "application/ld+json",
|
|
410
|
+
innerHTML: entry.text,
|
|
411
|
+
"data-camox-page-head": "true"
|
|
412
|
+
}));
|
|
322
413
|
return {
|
|
323
414
|
...title && { title },
|
|
324
415
|
meta: pageMeta,
|
|
325
|
-
link: pageLinks
|
|
416
|
+
link: pageLinks,
|
|
417
|
+
script: pageScripts
|
|
326
418
|
};
|
|
327
419
|
}
|
|
328
420
|
function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl, locale) {
|
|
@@ -400,16 +492,109 @@ function isPreviewHandoff(request, options) {
|
|
|
400
492
|
* The alternates of the page a request renders, where it exists in each of the scope's locales,
|
|
401
493
|
* and its head with their `hreflang` links. Without a head, there is none to add them to.
|
|
402
494
|
*/
|
|
403
|
-
async function readAlternates(request, options, { located, identity, head, ...reading }) {
|
|
404
|
-
const
|
|
405
|
-
const
|
|
495
|
+
async function readAlternates(request, options, { located, identity, head, described, ...reading }) {
|
|
496
|
+
const localeReads = requestLocaleReads(options, reading);
|
|
497
|
+
const [alternates, crumbs] = await Promise.all([findRequestAlternates(options, located, identity, localeReads), head ? readRequestBreadcrumb(options, located, described.title, localeReads) : []]);
|
|
498
|
+
const url = absoluteUrl(request, options);
|
|
499
|
+
const links = hreflangLinks(alternates, located.locales, url);
|
|
500
|
+
const place = {
|
|
501
|
+
url: url(localizedPath(located.path, located.locale)),
|
|
502
|
+
locale: located.locale.tag,
|
|
503
|
+
alternates
|
|
504
|
+
};
|
|
505
|
+
const site = {
|
|
506
|
+
url: url("/"),
|
|
507
|
+
name: described.project.name,
|
|
508
|
+
logo: projectLogo(url, described.project),
|
|
509
|
+
socialProfiles: described.project.socialProfiles ?? []
|
|
510
|
+
};
|
|
511
|
+
const breadcrumb = crumbs.map((crumb) => ({
|
|
512
|
+
name: crumb.name,
|
|
513
|
+
url: url(localizedPath(crumb.path, located.locale))
|
|
514
|
+
}));
|
|
515
|
+
const defaultLocale = located.locales.find((locale) => locale.isDefault) ?? located.locale;
|
|
516
|
+
const pageUrl = (marker) => {
|
|
517
|
+
const locale = located.locales.find((candidate) => candidate.tag === marker.locale);
|
|
518
|
+
return url(localizedPath(marker.path, locale ?? defaultLocale));
|
|
519
|
+
};
|
|
520
|
+
const blocks = withPageUrls(described.blocks ?? [], pageUrl);
|
|
521
|
+
const localeHome = url(localizedPath("/", located.locale));
|
|
522
|
+
const records = described.records && {
|
|
523
|
+
...described.records,
|
|
524
|
+
nodes: withPageUrls(described.records.nodes, pageUrl),
|
|
525
|
+
id: (recordId) => `${localeHome}#record-${recordId}`
|
|
526
|
+
};
|
|
406
527
|
return {
|
|
407
528
|
alternates,
|
|
408
|
-
head: head && withHreflangs(head, links)
|
|
529
|
+
head: head && withStructuredData(withLocatedTags(withHreflangs(head, links), place), place, site, {
|
|
530
|
+
...described,
|
|
531
|
+
breadcrumb,
|
|
532
|
+
blocks,
|
|
533
|
+
records
|
|
534
|
+
})
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/** An Open Graph locale: a locale tag with an underscore, as in `de_CH`. */
|
|
538
|
+
const ogLocale = (tag) => tag.replaceAll("-", "_");
|
|
539
|
+
/**
|
|
540
|
+
* A page's head with where it is: its canonical URL, which never names a draft, and its locale
|
|
541
|
+
* among the locales it exists in.
|
|
542
|
+
*/
|
|
543
|
+
function withLocatedTags(head, { url, locale, alternates }) {
|
|
544
|
+
const others = alternates.filter((alternate) => alternate.locale !== locale);
|
|
545
|
+
return {
|
|
546
|
+
...head,
|
|
547
|
+
meta: [
|
|
548
|
+
...head.meta ?? [],
|
|
549
|
+
{
|
|
550
|
+
property: "og:url",
|
|
551
|
+
content: url
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
property: "og:locale",
|
|
555
|
+
content: ogLocale(locale)
|
|
556
|
+
},
|
|
557
|
+
...others.map((other) => ({
|
|
558
|
+
property: "og:locale:alternate",
|
|
559
|
+
content: ogLocale(other.locale)
|
|
560
|
+
}))
|
|
561
|
+
],
|
|
562
|
+
links: [...head.links ?? [], {
|
|
563
|
+
rel: "canonical",
|
|
564
|
+
href: url
|
|
565
|
+
}]
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function requestLocaleReads(options, { authCookieHeader, draftId }) {
|
|
569
|
+
const { apiUrl, projectUuid } = options;
|
|
570
|
+
const reading = {
|
|
571
|
+
authCookieHeader: draftId ? authCookieHeader : void 0,
|
|
572
|
+
draftId
|
|
573
|
+
};
|
|
574
|
+
const source = draftId ? "draft" : "live";
|
|
575
|
+
const pagesByLocale = /* @__PURE__ */ new Map();
|
|
576
|
+
return {
|
|
577
|
+
pages: (locale) => {
|
|
578
|
+
if (!apiUrl || !projectUuid) return Promise.resolve([]);
|
|
579
|
+
const read = pagesByLocale.get(locale.tag) ?? createServerApiClient(apiUrl, {
|
|
580
|
+
...reading,
|
|
581
|
+
locale
|
|
582
|
+
}).pages.listBySlug({ projectUuid }).then(({ pages }) => pages);
|
|
583
|
+
pagesByLocale.set(locale.tag, read);
|
|
584
|
+
return read;
|
|
585
|
+
},
|
|
586
|
+
context: (locale) => createLayoutRequestContext(options, source, {
|
|
587
|
+
...reading,
|
|
588
|
+
locale
|
|
589
|
+
}),
|
|
590
|
+
records: (locale) => createRecordLookup(options, source, {
|
|
591
|
+
...reading,
|
|
592
|
+
locale
|
|
593
|
+
})
|
|
409
594
|
};
|
|
410
595
|
}
|
|
411
596
|
/** 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,
|
|
597
|
+
async function findRequestAlternates(options, located, identity, reads) {
|
|
413
598
|
const { apiUrl, projectUuid } = options;
|
|
414
599
|
const here = [{
|
|
415
600
|
locale: located.locale.tag,
|
|
@@ -417,26 +602,33 @@ async function findRequestAlternates(options, located, identity, { authCookieHea
|
|
|
417
602
|
}];
|
|
418
603
|
if (!apiUrl || !projectUuid || located.locales.length < 2) return here;
|
|
419
604
|
const app = await options.getCamoxApp?.();
|
|
420
|
-
const reading = {
|
|
421
|
-
authCookieHeader: draftId ? authCookieHeader : void 0,
|
|
422
|
-
draftId
|
|
423
|
-
};
|
|
424
605
|
return findAlternates({
|
|
425
606
|
identity,
|
|
426
607
|
located,
|
|
427
608
|
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
|
-
}
|
|
609
|
+
reads
|
|
438
610
|
});
|
|
439
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* The crumbs of the page a request renders, in its locale. Without them, the page still renders:
|
|
614
|
+
* a breadcrumb that can't be read is left out.
|
|
615
|
+
*/
|
|
616
|
+
async function readRequestBreadcrumb(options, located, title, reads) {
|
|
617
|
+
try {
|
|
618
|
+
return await readBreadcrumb({
|
|
619
|
+
path: located.path,
|
|
620
|
+
title,
|
|
621
|
+
app: await options.getCamoxApp?.(),
|
|
622
|
+
reads: {
|
|
623
|
+
pages: () => reads.pages(located.locale),
|
|
624
|
+
records: reads.records(located.locale)
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.error("[camox] Couldn't read the page's breadcrumb.", error);
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
440
632
|
/** A page's head with its alternates' `hreflang` links. */
|
|
441
633
|
function withHreflangs(head, hreflangs) {
|
|
442
634
|
if (!hreflangs.length) return head;
|
|
@@ -515,6 +707,7 @@ async function createPageHtmlResponse({ options, pathname, located, request }) {
|
|
|
515
707
|
pageId: result.data.page.page.id
|
|
516
708
|
},
|
|
517
709
|
head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : {},
|
|
710
|
+
described: curatedPageDescription(camoxApp, result.data.page, located.locale),
|
|
518
711
|
authCookieHeader: getServerAuthCookieHeader(request.headers),
|
|
519
712
|
draftId: result.draftId
|
|
520
713
|
});
|
|
@@ -662,7 +855,7 @@ async function createLayoutMarkdownResponse(options, match, locale, page) {
|
|
|
662
855
|
toMarkdown: template
|
|
663
856
|
})],
|
|
664
857
|
data,
|
|
665
|
-
|
|
858
|
+
record: recordPageValues(collection, record)
|
|
666
859
|
};
|
|
667
860
|
};
|
|
668
861
|
let rendered;
|
|
@@ -685,13 +878,14 @@ async function createLayoutMarkdownResponse(options, match, locale, page) {
|
|
|
685
878
|
});
|
|
686
879
|
const meta = buildPageMeta({
|
|
687
880
|
layout: match.layout._internal,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
881
|
+
context: buildLayoutContext(match.layout._internal, {
|
|
882
|
+
path: page.pathname,
|
|
883
|
+
locale: locale.tag,
|
|
884
|
+
projectName: slots.projectName,
|
|
885
|
+
data: rendered.data,
|
|
886
|
+
params: match.params,
|
|
887
|
+
record: "record" in rendered ? rendered.record : void 0
|
|
888
|
+
})
|
|
695
889
|
});
|
|
696
890
|
const markdown = [
|
|
697
891
|
slots.before,
|
|
@@ -746,6 +940,15 @@ async function createDerivedResponse(options, pathname, located, request, { data
|
|
|
746
940
|
}), serializedResult ?? null, shownIn);
|
|
747
941
|
const record = asCollectionItem(serializedResult);
|
|
748
942
|
const recordCollection = record && app.getCollectionById(record.collectionId);
|
|
943
|
+
const recordValues = record && recordCollection ? recordPageValues(recordCollection, record) : void 0;
|
|
944
|
+
const context = buildLayoutContext(match.layout._internal, {
|
|
945
|
+
path: pathname,
|
|
946
|
+
locale: located.locale.tag,
|
|
947
|
+
projectName: shared.projectName,
|
|
948
|
+
data: serializedResult?.data,
|
|
949
|
+
params: match.params,
|
|
950
|
+
record: recordValues
|
|
951
|
+
});
|
|
749
952
|
const { alternates, head: pageHead } = await readAlternates(request, options, {
|
|
750
953
|
located,
|
|
751
954
|
identity: record && match.layout._internal.kind === "derived" ? {
|
|
@@ -755,17 +958,30 @@ async function createDerivedResponse(options, pathname, located, request, { data
|
|
|
755
958
|
} : { kind: "code-owned" },
|
|
756
959
|
head: buildPageHead({
|
|
757
960
|
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,
|
|
961
|
+
context,
|
|
765
962
|
origin: new URL(request.url).origin,
|
|
766
963
|
faviconUrl: buildFaviconUrl(options.apiUrl, shared.project),
|
|
767
|
-
hasMarkdown: match.layout._internal.toMarkdown !== void 0
|
|
964
|
+
hasMarkdown: match.layout._internal.toMarkdown !== void 0,
|
|
965
|
+
recordType: record?.structuredData?.node?.["@type"]
|
|
768
966
|
}),
|
|
967
|
+
described: {
|
|
968
|
+
project: {
|
|
969
|
+
...shared.project,
|
|
970
|
+
name: shared.projectName
|
|
971
|
+
},
|
|
972
|
+
title: context.page.title,
|
|
973
|
+
declared: declarePage({
|
|
974
|
+
layout: match.layout._internal,
|
|
975
|
+
context
|
|
976
|
+
}),
|
|
977
|
+
blocks: placedBlockNodes(shared.layout, [], shared.structuredData),
|
|
978
|
+
...record?.published && { published: record.published },
|
|
979
|
+
records: pageRecords(shared.recordStructuredData, record && {
|
|
980
|
+
id: record.data.id,
|
|
981
|
+
structuredData: record.structuredData,
|
|
982
|
+
titles: recordValues?.titles
|
|
983
|
+
})
|
|
984
|
+
},
|
|
769
985
|
authCookieHeader,
|
|
770
986
|
draftId: read.draftId
|
|
771
987
|
});
|
|
@@ -794,6 +1010,11 @@ async function createDerivedResponse(options, pathname, located, request, { data
|
|
|
794
1010
|
locales: located.locales,
|
|
795
1011
|
derived: {
|
|
796
1012
|
layoutId: match.layout._internal.id,
|
|
1013
|
+
page: {
|
|
1014
|
+
params: match.params,
|
|
1015
|
+
path: pathname,
|
|
1016
|
+
projectName: shared.projectName
|
|
1017
|
+
},
|
|
797
1018
|
result: serializedResult,
|
|
798
1019
|
layout: shared.layout
|
|
799
1020
|
}
|
|
@@ -948,6 +1169,7 @@ async function createPageDataResponse({ options, pathname, located, request }) {
|
|
|
948
1169
|
pageId: result.data.page.page.id
|
|
949
1170
|
},
|
|
950
1171
|
head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
|
|
1172
|
+
described: curatedPageDescription(camoxApp, result.data.page, located.locale),
|
|
951
1173
|
authCookieHeader: getServerAuthCookieHeader(request.headers),
|
|
952
1174
|
draftId: result.draftId
|
|
953
1175
|
});
|
|
@@ -1000,6 +1222,7 @@ async function handleCamoxRequest(request, options = {}) {
|
|
|
1000
1222
|
});
|
|
1001
1223
|
}
|
|
1002
1224
|
if (match.kind === "og") return createOgResponse(request, options);
|
|
1225
|
+
if (match.kind === "logo") return createProjectLogoResponse(request, options);
|
|
1003
1226
|
if (match.kind === "sitemap") return createSitemapResponse(request, options);
|
|
1004
1227
|
if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
|
|
1005
1228
|
if (match.kind === "routes") return createRoutesResponse(request, options);
|
|
@@ -11,6 +11,7 @@ export declare const RUNTIME_ROUTE_PATHS: {
|
|
|
11
11
|
readonly health: "/_camox/health";
|
|
12
12
|
readonly registry: "/_camox/registry";
|
|
13
13
|
readonly routes: "/_camox/routes";
|
|
14
|
+
readonly logo: "/_camox/logo.png";
|
|
14
15
|
readonly og: "/og";
|
|
15
16
|
readonly sitemap: "/sitemap.xml";
|
|
16
17
|
readonly "llms-txt": "/llms.txt";
|