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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UNNAMED_DEFAULT_LOCALE, localeHeaders } from "../../lib/locale-path.js";
|
|
2
2
|
import { createDraftScope, withoutDraft } from "../../lib/draft-scope.js";
|
|
3
3
|
import { createStudioCache, reads } from "../../lib/studio-reads.js";
|
|
4
4
|
import { linkFallbackLocale } from "../../lib/link-pages.js";
|
|
5
|
+
import { curatedLayoutContext } from "../../core/layoutContext.js";
|
|
6
|
+
import { PAGE_IMAGE_SIZE, pageImageUrl } from "../og/pageImage.js";
|
|
7
|
+
import { isArticle } from "../../core/structuredData.js";
|
|
5
8
|
import { DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, NO_DEVELOPMENT_DRAFT_ERROR } from "@camox/api-contract/drafts";
|
|
6
9
|
import { createORPCClient } from "@orpc/client";
|
|
7
10
|
import { RPCLink } from "@orpc/client/fetch";
|
|
@@ -235,39 +238,25 @@ async function createMarkdownResponse({ apiUrl, camoxApp, locale, pathname, proj
|
|
|
235
238
|
return null;
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
|
-
/** A page's title and description, built by its layout's
|
|
241
|
+
/** A page's title and description, built by its layout's `toTitle` and `toDescription`. */
|
|
239
242
|
function buildPageMeta(input) {
|
|
240
|
-
const context =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
params: input.params,
|
|
244
|
-
projectName: input.projectName,
|
|
245
|
-
pageFullPath: input.pageFullPath
|
|
246
|
-
};
|
|
247
|
-
const title = input.layout?.buildMetaTitle({
|
|
248
|
-
...context,
|
|
249
|
-
pageMetaTitle: input.pageMetaTitle
|
|
250
|
-
}) ?? input.pageMetaTitle;
|
|
251
|
-
const description = input.layout?.buildMetaDescription ? input.layout.buildMetaDescription({
|
|
252
|
-
...context,
|
|
253
|
-
pageMetaDescription: input.pageMetaDescription
|
|
254
|
-
}) : input.pageMetaDescription;
|
|
243
|
+
const { layout, context } = input;
|
|
244
|
+
const title = layout ? layout.toTitle(context) : context.page.title;
|
|
245
|
+
const description = layout ? layout.toDescription(context) : context.page.description;
|
|
255
246
|
return {
|
|
256
247
|
title,
|
|
257
248
|
...description && { description }
|
|
258
249
|
};
|
|
259
250
|
}
|
|
260
|
-
/**
|
|
251
|
+
/** What a page's layout says the page is, from the page's context. */
|
|
252
|
+
function declarePage(input) {
|
|
253
|
+
return input.layout?.toStructuredData(input.context);
|
|
254
|
+
}
|
|
261
255
|
/** A curated page's meta input. Its path keeps the prefix of the page's `locale`. */
|
|
262
|
-
function curatedPageMetaInput(camoxApp, page, locale) {
|
|
256
|
+
function curatedPageMetaInput(camoxApp, page, locale = UNNAMED_DEFAULT_LOCALE) {
|
|
263
257
|
return {
|
|
264
258
|
layout: page.layout ? camoxApp?.getLayoutById(page.layout.layoutId)?._internal : void 0,
|
|
265
|
-
|
|
266
|
-
pageMetaDescription: page.page.metaDescription ?? void 0,
|
|
267
|
-
data: void 0,
|
|
268
|
-
params: {},
|
|
269
|
-
projectName: page.projectName,
|
|
270
|
-
pageFullPath: locale ? localizedPath(page.page.fullPath, locale) : page.page.fullPath
|
|
259
|
+
context: curatedLayoutContext(page, locale)
|
|
271
260
|
};
|
|
272
261
|
}
|
|
273
262
|
/** A Markdown Representation, opened by YAML front matter stating the page's title, description and URL. */
|
|
@@ -281,18 +270,18 @@ function withFrontMatter(markdown, meta) {
|
|
|
281
270
|
/** The head every page gets, whatever its layout kind. */
|
|
282
271
|
function buildPageHead(input) {
|
|
283
272
|
const { title, description } = buildPageMeta(input);
|
|
273
|
+
const mainEntity = declarePage(input) ?? (input.recordType ? { "@type": input.recordType } : void 0);
|
|
284
274
|
const meta = [{ title }];
|
|
285
275
|
if (description) meta.push({
|
|
286
276
|
name: "description",
|
|
287
277
|
content: description
|
|
288
278
|
});
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
279
|
+
const { page, project } = input.context;
|
|
280
|
+
const ogImageUrl = pageImageUrl({
|
|
281
|
+
origin: input.origin,
|
|
282
|
+
path: page.path,
|
|
283
|
+
customImageUrl: input.customOgImageUrl
|
|
294
284
|
});
|
|
295
|
-
const ogImageUrl = input.customOgImageUrl ?? `${input.origin}/og?${ogImageParams.toString()}`;
|
|
296
285
|
meta.push({
|
|
297
286
|
property: "og:title",
|
|
298
287
|
content: title
|
|
@@ -301,15 +290,25 @@ function buildPageHead(input) {
|
|
|
301
290
|
content: ogImageUrl
|
|
302
291
|
}, {
|
|
303
292
|
property: "og:image:width",
|
|
304
|
-
content:
|
|
293
|
+
content: String(PAGE_IMAGE_SIZE.width)
|
|
305
294
|
}, {
|
|
306
295
|
property: "og:image:height",
|
|
307
|
-
content:
|
|
296
|
+
content: String(PAGE_IMAGE_SIZE.height)
|
|
308
297
|
});
|
|
309
298
|
if (description) meta.push({
|
|
310
299
|
property: "og:description",
|
|
311
300
|
content: description
|
|
312
301
|
});
|
|
302
|
+
meta.push({
|
|
303
|
+
property: "og:type",
|
|
304
|
+
content: isArticle(mainEntity ?? { "@type": "" }) ? "article" : "website"
|
|
305
|
+
}, {
|
|
306
|
+
property: "og:site_name",
|
|
307
|
+
content: project.name
|
|
308
|
+
}, {
|
|
309
|
+
name: "twitter:card",
|
|
310
|
+
content: "summary_large_image"
|
|
311
|
+
});
|
|
313
312
|
const links = [{
|
|
314
313
|
rel: "icon",
|
|
315
314
|
href: input.faviconUrl
|
|
@@ -317,7 +316,7 @@ function buildPageHead(input) {
|
|
|
317
316
|
if (input.hasMarkdown) links.push({
|
|
318
317
|
rel: "alternate",
|
|
319
318
|
type: "text/markdown",
|
|
320
|
-
href: `${input.origin}${getMarkdownPath(
|
|
319
|
+
href: `${input.origin}${getMarkdownPath(page.path)}`
|
|
321
320
|
});
|
|
322
321
|
return {
|
|
323
322
|
meta,
|
|
@@ -328,10 +327,8 @@ function buildPageHead(input) {
|
|
|
328
327
|
function buildCamoxPageHead(camoxApp, loaderData, locale) {
|
|
329
328
|
if (!loaderData) return {};
|
|
330
329
|
const { page, origin, faviconUrl } = loaderData;
|
|
331
|
-
const metaInput = curatedPageMetaInput(camoxApp, page, locale);
|
|
332
330
|
return buildPageHead({
|
|
333
|
-
...
|
|
334
|
-
ogImageTitle: metaInput.pageMetaTitle,
|
|
331
|
+
...curatedPageMetaInput(camoxApp, page, locale),
|
|
335
332
|
customOgImageUrl: page.page.customOgImageUrl,
|
|
336
333
|
origin,
|
|
337
334
|
faviconUrl,
|
|
@@ -340,4 +337,4 @@ function buildCamoxPageHead(camoxApp, loaderData, locale) {
|
|
|
340
337
|
}
|
|
341
338
|
|
|
342
339
|
//#endregion
|
|
343
|
-
export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, loadCamoxPageForRequest, parseMarkdownPath, parseQuality, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter };
|
|
340
|
+
export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, curatedPageMetaInput, declarePage, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, loadCamoxPageForRequest, parseMarkdownPath, parseQuality, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { previewStore, selectIsCanvasShown } from "../preview/previewStore.js";
|
|
2
2
|
import { shownRecordPage } from "../preview/pageRecord.js";
|
|
3
3
|
import { Navbar } from "../studio/components/Navbar.js";
|
|
4
|
-
import { PreviewToolbar } from "../preview/components/PreviewToolbar.js";
|
|
5
4
|
import { EditingFallbackContext } from "./editingFallback.js";
|
|
5
|
+
import { PreviewToolbar } from "../preview/components/PreviewToolbar.js";
|
|
6
6
|
import { StudioEditingFallback } from "../preview/components/StudioEditingFallback.js";
|
|
7
7
|
import { ChatSidebar } from "../studio/components/ChatSidebar.js";
|
|
8
8
|
import { FollowAgentProvider } from "../studio/FollowAgentProvider.js";
|
|
@@ -22,7 +22,7 @@ function RuntimeChrome(t0) {
|
|
|
22
22
|
const website = !input.routeKind;
|
|
23
23
|
const hidden = useSelector(previewStore, _temp);
|
|
24
24
|
const isCanvasShown = useSelector(previewStore, selectIsCanvasShown);
|
|
25
|
-
const showChat =
|
|
25
|
+
const showChat = website && isCanvasShown;
|
|
26
26
|
const t1 = input.runtimeBasePath;
|
|
27
27
|
let t2;
|
|
28
28
|
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { recordPageValues } from "../../core/recordItem.js";
|
|
2
|
+
import { curatedPageTitle } from "../../core/layoutContext.js";
|
|
3
|
+
import { getPageDestinations } from "../../core/pageDestinations.js";
|
|
4
|
+
import { matchDerivedLayout } from "../../core/derivedRoutes.js";
|
|
5
|
+
|
|
6
|
+
//#region src/features/runtime/breadcrumbs.ts
|
|
7
|
+
/** The paths above a page's, from the home page down: none for the home page itself. */
|
|
8
|
+
function ancestorPaths(path) {
|
|
9
|
+
const segments = path.split("/").filter(Boolean);
|
|
10
|
+
if (segments.length === 0) return [];
|
|
11
|
+
return ["/", ...segments.slice(1).map((_, index) => `/${segments.slice(0, index + 1).join("/")}`)];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A page's crumbs, ending with the page itself, which `title` names. The home page has none. An
|
|
15
|
+
* ancestor that isn't a page, or is a derived page other than a record page, is left out.
|
|
16
|
+
*/
|
|
17
|
+
async function readBreadcrumb({ path, title, app, reads }) {
|
|
18
|
+
const ancestors = ancestorPaths(path);
|
|
19
|
+
if (ancestors.length === 0) return [];
|
|
20
|
+
const layouts = app?.getLayouts() ?? [];
|
|
21
|
+
const pages = await reads.pages();
|
|
22
|
+
const destinations = getPageDestinations(pages, layouts);
|
|
23
|
+
const pagesByPath = new Map(pages.map((page) => [page.fullPath, page]));
|
|
24
|
+
return [...(await Promise.all(ancestors.map(async (ancestor) => {
|
|
25
|
+
const destination = destinations.find((d) => d.kind === "singleton" && d.fullPath === ancestor) ?? destinations.find((d) => d.kind === "curated" && d.fullPath === ancestor);
|
|
26
|
+
if (destination?.kind === "singleton") return {
|
|
27
|
+
name: destination.title,
|
|
28
|
+
path: ancestor
|
|
29
|
+
};
|
|
30
|
+
if (destination?.kind === "curated") {
|
|
31
|
+
const page = pagesByPath.get(ancestor);
|
|
32
|
+
return {
|
|
33
|
+
name: curatedPageTitle(page),
|
|
34
|
+
path: ancestor
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const name = await recordPageTitle(app, ancestor, reads.records);
|
|
38
|
+
return name === void 0 ? void 0 : {
|
|
39
|
+
name,
|
|
40
|
+
path: ancestor
|
|
41
|
+
};
|
|
42
|
+
}))).filter((crumb) => crumb !== void 0), {
|
|
43
|
+
name: title,
|
|
44
|
+
path
|
|
45
|
+
}];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The title of the record a record page at `path` renders, found by its route param as the
|
|
49
|
+
* page's own read finds it. Undefined when no record page is there.
|
|
50
|
+
*/
|
|
51
|
+
async function recordPageTitle(app, path, records) {
|
|
52
|
+
const match = app && matchDerivedLayout(app.getLayouts(), path);
|
|
53
|
+
const recordPages = match?.layout._internal.recordPages;
|
|
54
|
+
if (!match || !recordPages || !records) return void 0;
|
|
55
|
+
const value = recordPages.param ? match.params[recordPages.param] : Object.values(match.params).length === 1 ? Object.values(match.params)[0] : void 0;
|
|
56
|
+
const collection = app.getCollectionById(recordPages.collectionId);
|
|
57
|
+
if (!value || !collection) return void 0;
|
|
58
|
+
const found = await records.findByField({
|
|
59
|
+
collectionId: recordPages.collectionId,
|
|
60
|
+
field: recordPages.field,
|
|
61
|
+
value
|
|
62
|
+
});
|
|
63
|
+
if (found.length !== 1) return void 0;
|
|
64
|
+
return recordPageValues(collection, { data: found[0] }).title;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { readBreadcrumb };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequestContext } from "../../core/requestContext.js";
|
|
2
|
+
import { curatedPageTitle } from "../../core/layoutContext.js";
|
|
2
3
|
import { getPageDestinations } from "../../core/pageDestinations.js";
|
|
3
4
|
import { discoverLayoutPages } from "../../core/derivedRoutes.js";
|
|
4
5
|
|
|
@@ -27,7 +28,7 @@ async function listLivePages({ pages, app, records }) {
|
|
|
27
28
|
key: destination.key,
|
|
28
29
|
fullPath: page.fullPath,
|
|
29
30
|
layoutId: page.layoutId,
|
|
30
|
-
title: page
|
|
31
|
+
title: curatedPageTitle(page),
|
|
31
32
|
...page.metaDescription && { description: page.metaDescription },
|
|
32
33
|
hasMarkdown: true,
|
|
33
34
|
updatedAt: page.updatedAt
|
|
@@ -41,7 +42,7 @@ async function listLivePages({ pages, app, records }) {
|
|
|
41
42
|
key: page.recordId ? `record:${layout._internal.id}:${page.recordId}` : `derived:${page.path}`,
|
|
42
43
|
fullPath: page.path,
|
|
43
44
|
layoutId: layout._internal.id,
|
|
44
|
-
title: page.
|
|
45
|
+
title: page.title || page.path,
|
|
45
46
|
hasMarkdown: servesMarkdown(layout)
|
|
46
47
|
}));
|
|
47
48
|
}));
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useAuthState, useSignInRedirect } from "../../lib/auth.js";
|
|
2
2
|
import { DraftScopeContext } from "../../lib/draft-scope.js";
|
|
3
3
|
import { DerivedPageContent } from "../page/DerivedPageContent.js";
|
|
4
|
+
import { useSignInShortcut } from "../provider/signInShortcut.js";
|
|
4
5
|
import { PreviewDocumentContext } from "./PreviewDocumentContext.js";
|
|
5
6
|
import { PreviewActivation } from "../preview/components/PreviewActivation.js";
|
|
6
7
|
import { Frame } from "../preview/components/Frame.js";
|
|
7
8
|
import { PreviewSignInPage } from "../provider/PreviewSignInPage.js";
|
|
8
|
-
import { CoreCamoxProvider
|
|
9
|
+
import { CoreCamoxProvider } from "../provider/CoreCamoxProvider.js";
|
|
9
10
|
import { CuratedPageContent } from "../page/CuratedPageContent.js";
|
|
10
11
|
import { EditingFallbackSlot } from "./editingFallback.js";
|
|
11
12
|
import { PageNavigationProvider } from "./pageNavigation.js";
|
|
@@ -96,7 +97,6 @@ function PageApp(t0) {
|
|
|
96
97
|
return t6;
|
|
97
98
|
}
|
|
98
99
|
const LazyEditablePageExperience = React.lazy(() => import("../preview/EditablePageExperience.js").then((module) => ({ default: module.EditablePageExperience })));
|
|
99
|
-
const LazyLocalhostPreviewProvider = React.lazy(() => import("../provider/LocalhostPreviewProvider.js").then((module) => ({ default: module.LocalhostPreviewProvider })));
|
|
100
100
|
/** Editing lists and adds any block, so it waits for every definition, not only the page's. */
|
|
101
101
|
function AllBlocks({ camoxApp, children }) {
|
|
102
102
|
React.use(camoxApp.loadAllBlocks());
|
|
@@ -116,7 +116,7 @@ var EditingActivationBoundary = class extends React.Component {
|
|
|
116
116
|
};
|
|
117
117
|
const subscribeToHydration = () => () => {};
|
|
118
118
|
function PageExperience(t0) {
|
|
119
|
-
const $ = c(
|
|
119
|
+
const $ = c(38);
|
|
120
120
|
const { camoxApp, input, queryClient, studioDocument } = t0;
|
|
121
121
|
const { isAuthenticated, isLoading } = useAuthState();
|
|
122
122
|
const hasHydrated = React.useSyncExternalStore(subscribeToHydration, _temp, _temp2);
|
|
@@ -145,6 +145,7 @@ function PageExperience(t0) {
|
|
|
145
145
|
t2 = $[5];
|
|
146
146
|
}
|
|
147
147
|
React.useEffect(t1, t2);
|
|
148
|
+
useSignInShortcut(hasHydrated && !studioOnly && !isAuthenticated && !isLoading);
|
|
148
149
|
let t3;
|
|
149
150
|
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
|
150
151
|
t3 = /* @__PURE__ */ jsx("div", {
|
|
@@ -248,50 +249,21 @@ function PageExperience(t0) {
|
|
|
248
249
|
return t11;
|
|
249
250
|
}
|
|
250
251
|
if (studioOnly) return null;
|
|
251
|
-
if (hasHydrated && isLocalhostPreview()) {
|
|
252
|
-
let t7;
|
|
253
|
-
if ($[35] !== readOnlyPage) {
|
|
254
|
-
t7 = /* @__PURE__ */ jsx(LazyLocalhostPreviewProvider, { children: readOnlyPage });
|
|
255
|
-
$[35] = readOnlyPage;
|
|
256
|
-
$[36] = t7;
|
|
257
|
-
} else t7 = $[36];
|
|
258
|
-
let t8;
|
|
259
|
-
if ($[37] !== camoxApp || $[38] !== t7) {
|
|
260
|
-
t8 = /* @__PURE__ */ jsx(AllBlocks, {
|
|
261
|
-
camoxApp,
|
|
262
|
-
children: t7
|
|
263
|
-
});
|
|
264
|
-
$[37] = camoxApp;
|
|
265
|
-
$[38] = t7;
|
|
266
|
-
$[39] = t8;
|
|
267
|
-
} else t8 = $[39];
|
|
268
|
-
let t9;
|
|
269
|
-
if ($[40] !== readOnlyPage || $[41] !== t8) {
|
|
270
|
-
t9 = /* @__PURE__ */ jsx(React.Suspense, {
|
|
271
|
-
fallback: readOnlyPage,
|
|
272
|
-
children: t8
|
|
273
|
-
});
|
|
274
|
-
$[40] = readOnlyPage;
|
|
275
|
-
$[41] = t8;
|
|
276
|
-
$[42] = t9;
|
|
277
|
-
} else t9 = $[42];
|
|
278
|
-
return t9;
|
|
279
|
-
}
|
|
280
252
|
if (studioDocument) {
|
|
281
253
|
let t7;
|
|
282
|
-
if ($[
|
|
254
|
+
if ($[35] === Symbol.for("react.memo_cache_sentinel")) {
|
|
283
255
|
t7 = { height: "100vh" };
|
|
284
|
-
$[
|
|
285
|
-
} else t7 = $[
|
|
256
|
+
$[35] = t7;
|
|
257
|
+
} else t7 = $[35];
|
|
286
258
|
let t8;
|
|
287
|
-
if ($[
|
|
259
|
+
if ($[36] !== isolatedPage) {
|
|
288
260
|
t8 = /* @__PURE__ */ jsx("div", {
|
|
289
261
|
style: t7,
|
|
290
262
|
children: isolatedPage
|
|
291
263
|
});
|
|
292
|
-
$[
|
|
293
|
-
$[
|
|
294
|
-
} else t8 = $[
|
|
264
|
+
$[36] = isolatedPage;
|
|
265
|
+
$[37] = t8;
|
|
266
|
+
} else t8 = $[37];
|
|
295
267
|
return t8;
|
|
296
268
|
}
|
|
297
269
|
return readOnlyPage;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { curatedPageTitle } from "../../core/layoutContext.js";
|
|
2
|
+
import { curatedPageMetaInput, declarePage } from "../routes/pageRuntime.js";
|
|
3
|
+
import { buildPageGraph, serializeStructuredData } from "./structuredData.js";
|
|
4
|
+
|
|
5
|
+
//#region src/features/runtime/pageGraph.ts
|
|
6
|
+
/**
|
|
7
|
+
* The records a page's graph can hold: those its blocks reference, and on a record page, the
|
|
8
|
+
* page's record and those it references.
|
|
9
|
+
*/
|
|
10
|
+
function pageRecords(blocks, page) {
|
|
11
|
+
const own = page?.structuredData;
|
|
12
|
+
return {
|
|
13
|
+
nodes: {
|
|
14
|
+
...blocks?.nodes,
|
|
15
|
+
...own?.records.nodes,
|
|
16
|
+
...page && own?.node && { [page.id]: own.node }
|
|
17
|
+
},
|
|
18
|
+
referenced: [.../* @__PURE__ */ new Set([...blocks?.referenced ?? [], ...own?.records.referenced ?? []])],
|
|
19
|
+
...page?.titles && { titles: page.titles },
|
|
20
|
+
...page && { main: page.id }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Each placed block's nodes, its layout's before and after the page's own. */
|
|
24
|
+
function placedBlockNodes(layout, pageBlockIds, structuredData) {
|
|
25
|
+
if (!structuredData) return [];
|
|
26
|
+
return [
|
|
27
|
+
...layout?.beforeBlockIds ?? [],
|
|
28
|
+
...pageBlockIds,
|
|
29
|
+
...layout?.afterBlockIds ?? []
|
|
30
|
+
].flatMap((id) => {
|
|
31
|
+
const nodes = structuredData?.[id];
|
|
32
|
+
return nodes?.length ? [{
|
|
33
|
+
id,
|
|
34
|
+
nodes
|
|
35
|
+
}] : [];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** What a curated page's Structured Data says beyond its head. */
|
|
39
|
+
function curatedPageDescription(camoxApp, structure, locale) {
|
|
40
|
+
const { page, project, projectName } = structure;
|
|
41
|
+
return {
|
|
42
|
+
project: {
|
|
43
|
+
...project,
|
|
44
|
+
name: projectName
|
|
45
|
+
},
|
|
46
|
+
title: curatedPageTitle(page),
|
|
47
|
+
published: {
|
|
48
|
+
first: page.firstPublishedAt,
|
|
49
|
+
last: page.publishedAt
|
|
50
|
+
},
|
|
51
|
+
declared: declarePage(curatedPageMetaInput(camoxApp, structure, locale)),
|
|
52
|
+
blocks: placedBlockNodes(structure.layout, page.blockIds, structure.structuredData),
|
|
53
|
+
records: pageRecords(structure.recordStructuredData)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The value of a head's meta tag, by its name or property. */
|
|
57
|
+
function headMeta(head, key) {
|
|
58
|
+
return head.meta?.find((entry) => entry.name === key || entry.property === key)?.content;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A page's head with its Structured Data, which names and describes the page as the head does,
|
|
62
|
+
* with the head's page image.
|
|
63
|
+
*/
|
|
64
|
+
function withStructuredData(head, place, site, { published, declared, breadcrumb, blocks, records }) {
|
|
65
|
+
const name = head.meta?.find((entry) => entry.title)?.title ?? "";
|
|
66
|
+
const graph = buildPageGraph({
|
|
67
|
+
site,
|
|
68
|
+
page: {
|
|
69
|
+
url: place.url,
|
|
70
|
+
name,
|
|
71
|
+
description: headMeta(head, "description"),
|
|
72
|
+
inLanguage: place.locale,
|
|
73
|
+
image: headMeta(head, "og:image"),
|
|
74
|
+
...published?.first && { datePublished: new Date(published.first) },
|
|
75
|
+
...published?.last && { dateModified: new Date(published.last) },
|
|
76
|
+
breadcrumb
|
|
77
|
+
},
|
|
78
|
+
declared,
|
|
79
|
+
blocks,
|
|
80
|
+
records
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
...head,
|
|
84
|
+
scripts: [...head.scripts ?? [], {
|
|
85
|
+
type: "application/ld+json",
|
|
86
|
+
text: serializeStructuredData(graph)
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { curatedPageDescription, pageRecords, placedBlockNodes, withStructuredData };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { RUNTIME_ROUTE_PATHS } from "./runtimeRoutes.js";
|
|
2
|
+
import { buildDefaultFaviconUrl, buildFaviconUrl, createServerApiClient, isNotFoundError } from "../routes/pageRuntime.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/features/runtime/projectLogo.tsx
|
|
6
|
+
/**
|
|
7
|
+
* The project's logo in its Structured Data: its favicon, rendered as a PNG by the runtime.
|
|
8
|
+
* Search engines want a raster logo at least 112px wide, and favicons are often SVGs or tiny.
|
|
9
|
+
*/
|
|
10
|
+
/** The size the logo is served at: square, and wider than search engines' minimum. */
|
|
11
|
+
const LOGO_SIZE = 512;
|
|
12
|
+
/**
|
|
13
|
+
* Where the runtime serves its project's logo. `url` makes a runtime path absolute. The project's
|
|
14
|
+
* version only busts caches: the route always serves the project's current logo.
|
|
15
|
+
*/
|
|
16
|
+
function projectLogo(url, project) {
|
|
17
|
+
const search = new URLSearchParams({ v: String(project.updatedAt) });
|
|
18
|
+
return {
|
|
19
|
+
url: `${url(RUNTIME_ROUTE_PATHS.logo)}?${search}`,
|
|
20
|
+
width: 512,
|
|
21
|
+
height: 512
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** An image fetched as a data URL, which the renderer reads without fetching it again. */
|
|
25
|
+
async function asDataUrl(url) {
|
|
26
|
+
const response = await fetch(new Request(url));
|
|
27
|
+
if (!response.ok) return null;
|
|
28
|
+
const type = response.headers.get("Content-Type") ?? "application/octet-stream";
|
|
29
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
30
|
+
let binary = "";
|
|
31
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
32
|
+
return `data:${type};base64,${btoa(binary)}`;
|
|
33
|
+
}
|
|
34
|
+
async function renderLogo(source) {
|
|
35
|
+
const { ImageResponse } = await import("camox/_internal/imageResponse");
|
|
36
|
+
return new ImageResponse(/* @__PURE__ */ jsx("div", {
|
|
37
|
+
style: {
|
|
38
|
+
display: "flex",
|
|
39
|
+
width: "100%",
|
|
40
|
+
height: "100%"
|
|
41
|
+
},
|
|
42
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
43
|
+
src: source,
|
|
44
|
+
style: {
|
|
45
|
+
width: "100%",
|
|
46
|
+
height: "100%",
|
|
47
|
+
objectFit: "contain"
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}), {
|
|
51
|
+
width: 512,
|
|
52
|
+
height: 512,
|
|
53
|
+
format: "png"
|
|
54
|
+
}).arrayBuffer();
|
|
55
|
+
}
|
|
56
|
+
/** The project's favicon as a PNG. A favicon the renderer can't read gives way to the default. */
|
|
57
|
+
async function renderProjectLogo(apiUrl, project) {
|
|
58
|
+
for (const url of [buildFaviconUrl(apiUrl, project), buildDefaultFaviconUrl(apiUrl)]) {
|
|
59
|
+
const source = await asDataUrl(url);
|
|
60
|
+
if (!source) continue;
|
|
61
|
+
try {
|
|
62
|
+
return await renderLogo(source);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The last logo rendered, with the version of the project it was rendered for, by project. A
|
|
69
|
+
* runtime serves one project, so it keeps one logo, and renders again only once the project
|
|
70
|
+
* changed.
|
|
71
|
+
*/
|
|
72
|
+
const renderedLogos = /* @__PURE__ */ new Map();
|
|
73
|
+
/**
|
|
74
|
+
* The website's own project's logo, as a PNG. The request names no project, and its `v` only
|
|
75
|
+
* busts caches: whatever it says, the logo is the project's current one, rendered once per
|
|
76
|
+
* version of the project.
|
|
77
|
+
*/
|
|
78
|
+
async function createProjectLogoResponse(request, { apiUrl, projectUuid }) {
|
|
79
|
+
if (!apiUrl || !projectUuid) return new Response("Not found", { status: 404 });
|
|
80
|
+
let project;
|
|
81
|
+
try {
|
|
82
|
+
project = await createServerApiClient(apiUrl).projects.getSummary({ projectUuid });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (isNotFoundError(error)) return new Response("Not found", { status: 404 });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const version = String(project.updatedAt);
|
|
88
|
+
const key = `${apiUrl} ${projectUuid}`;
|
|
89
|
+
let rendered = renderedLogos.get(key);
|
|
90
|
+
if (rendered?.version !== version) {
|
|
91
|
+
rendered = {
|
|
92
|
+
version,
|
|
93
|
+
png: renderProjectLogo(apiUrl, project)
|
|
94
|
+
};
|
|
95
|
+
renderedLogos.set(key, rendered);
|
|
96
|
+
}
|
|
97
|
+
const png = await rendered.png;
|
|
98
|
+
if (!png) {
|
|
99
|
+
if (renderedLogos.get(key) === rendered) renderedLogos.delete(key);
|
|
100
|
+
return new Response("Not found", { status: 404 });
|
|
101
|
+
}
|
|
102
|
+
const current = new URL(request.url).searchParams.get("v") === version;
|
|
103
|
+
return new Response(png, { headers: {
|
|
104
|
+
"Content-Type": "image/png",
|
|
105
|
+
"Cache-Control": current ? "public, max-age=86400" : "public, no-cache"
|
|
106
|
+
} });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
export { createProjectLogoResponse, projectLogo };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CamoxDocument } from "../../core/defineDocument.js";
|
|
2
|
+
import { AlternatePath, LocaleView } from "../../lib/locale-path.js";
|
|
2
3
|
import { AnyLayoutLoaderResult } from "../../core/createLayout.js";
|
|
3
4
|
import { CamoxApp } from "../../core/createApp.js";
|
|
4
|
-
import { AlternatePath, LocaleView } from "../../lib/locale-path.js";
|
|
5
5
|
import { AnalyticsProviders } from "./analytics.js";
|
|
6
6
|
import { RuntimeOwnedRouteKind } from "./runtimeRoutes.js";
|
|
7
7
|
import { StudioRenderInput } from "./studioApp.js";
|
|
8
|
-
import { DraftRead, MemberProject, createServerApiClient } from "../routes/pageRuntime.js";
|
|
8
|
+
import { DraftRead, MemberProject, PageHead, createServerApiClient } from "../routes/pageRuntime.js";
|
|
9
9
|
import { UseHeadInput } from "unhead/types";
|
|
10
10
|
//#region src/features/runtime/runtime.d.ts
|
|
11
11
|
export type RuntimeRouteKind = RuntimeOwnedRouteKind | "canvas" | "page" | "studio" | "studio-content" | "studio-nested";
|
|
@@ -39,6 +39,14 @@ export interface PageRenderInput extends DraftRead {
|
|
|
39
39
|
presentation?: "public" | "studio";
|
|
40
40
|
derived?: {
|
|
41
41
|
layoutId: string;
|
|
42
|
+
/** What the layout's context names, beside the loader's result. */
|
|
43
|
+
page?: {
|
|
44
|
+
/** The params the page's route matched. */
|
|
45
|
+
params: Record<string, string>;
|
|
46
|
+
/** The page's path, with its locale's prefix. */
|
|
47
|
+
path: string;
|
|
48
|
+
projectName: string;
|
|
49
|
+
};
|
|
42
50
|
result?: AnyLayoutLoaderResult;
|
|
43
51
|
layout: Awaited<ReturnType<ReturnType<typeof createServerApiClient>["layouts"]["get"]>>["layout"];
|
|
44
52
|
};
|
|
@@ -89,9 +97,6 @@ export interface RuntimeOptions {
|
|
|
89
97
|
}
|
|
90
98
|
export declare function getRuntimePathname(url: string, runtimeBasePath?: string): string | null;
|
|
91
99
|
export declare function matchRuntimeRoute(pathname: string): RuntimeRouteMatch;
|
|
92
|
-
export declare function createPageHeadInput(head:
|
|
93
|
-
meta?: Array<Record<string, string>>;
|
|
94
|
-
links?: Array<Record<string, string>>;
|
|
95
|
-
}): UseHeadInput;
|
|
100
|
+
export declare function createPageHeadInput(head: PageHead): UseHeadInput;
|
|
96
101
|
export declare function handleCamoxRequest(request: Request, options?: RuntimeOptions): Promise<Response | null>;
|
|
97
102
|
//#endregion
|