@aglyn/tenant-runtime 1.0.0-beta.153 → 1.0.0-beta.155
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/package.json +3 -3
- package/src/lib/collection-fallback-nodes.d.ts +5 -1
- package/src/lib/collection-fallback-nodes.js +4 -4
- package/src/lib/collection-fallback-nodes.js.map +1 -1
- package/src/lib/compose-author-page.d.ts +10 -0
- package/src/lib/compose-author-page.js +4 -2
- package/src/lib/compose-author-page.js.map +1 -1
- package/src/lib/compose-collection-page.d.ts +10 -0
- package/src/lib/compose-collection-page.js +8 -4
- package/src/lib/compose-collection-page.js.map +1 -1
- package/src/lib/compose-screen-nodes.d.ts +20 -0
- package/src/lib/compose-screen-nodes.js +9 -7
- package/src/lib/compose-screen-nodes.js.map +1 -1
- package/src/lib/compose-search-page.d.ts +5 -0
- package/src/lib/compose-search-page.js +4 -2
- package/src/lib/compose-search-page.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-runtime",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.155",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.155",
|
|
29
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.155",
|
|
30
30
|
"@swc/helpers": "0.5.23"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
@@ -56,7 +56,9 @@ export declare function buildCollectionEntryFallbackNodes(collection: FallbackCo
|
|
|
56
56
|
* because a bare org reference still resolves without it; passing it is what
|
|
57
57
|
* lets an asset restricted to this site resolve at all.
|
|
58
58
|
*/
|
|
59
|
-
hostId?: string
|
|
59
|
+
hostId?: string,
|
|
60
|
+
/** The site's zone (AGL-3237); UTC when a site has not named one. */
|
|
61
|
+
timeZone?: string): NodesMap;
|
|
60
62
|
/** Pagination state for the built-in list (AGL-620). */
|
|
61
63
|
export interface FallbackListPagination {
|
|
62
64
|
page: number;
|
|
@@ -91,5 +93,7 @@ export declare function buildCollectionFallbackNodes(content: {
|
|
|
91
93
|
category?: FallbackListCategory | null;
|
|
92
94
|
/** The composing site, for host-qualifying a media reference (AGL-1407). */
|
|
93
95
|
hostId?: string;
|
|
96
|
+
/** The site's zone (AGL-3237); UTC when a site has not named one. */
|
|
97
|
+
timeZone?: string;
|
|
94
98
|
}): NodesMap;
|
|
95
99
|
export default buildCollectionFallbackNodes;
|
|
@@ -23,7 +23,7 @@ const id = (suffix)=>`${FALLBACK_ID_PREFIX}${suffix}`;
|
|
|
23
23
|
// mismatch — but it was a THIRD spelling of "the entry's published date",
|
|
24
24
|
// free to drift from the byline and the related-post card that quote the
|
|
25
25
|
// same instant. Pinned locale and zone, so all three agree by construction.
|
|
26
|
-
const formatDate = (value)=>Aglyn.formatCollectionEntryDate(value);
|
|
26
|
+
const formatDate = (value, timeZone)=>Aglyn.formatCollectionEntryDate(value, undefined, undefined, timeZone);
|
|
27
27
|
/**
|
|
28
28
|
* What may be interpolated into the cover block's CSS `url("…")` (AGL-1407).
|
|
29
29
|
*
|
|
@@ -237,7 +237,7 @@ const typography = (suffix, props)=>[
|
|
|
237
237
|
* way every other render surface qualifies one (AGL-1043/AGL-1407). Optional
|
|
238
238
|
* because a bare org reference still resolves without it; passing it is what
|
|
239
239
|
* lets an asset restricted to this site resolve at all.
|
|
240
|
-
*/ hostId) {
|
|
240
|
+
*/ hostId, /** The site's zone (AGL-3237); UTC when a site has not named one. */ timeZone) {
|
|
241
241
|
var _entry_title, _entry_tags, _Aglyn_resolveEntryCategoryName, _entry_body;
|
|
242
242
|
const entries = [
|
|
243
243
|
typography('title', {
|
|
@@ -246,7 +246,7 @@ const typography = (suffix, props)=>[
|
|
|
246
246
|
children: (_entry_title = entry.title) != null ? _entry_title : ''
|
|
247
247
|
})
|
|
248
248
|
];
|
|
249
|
-
const date = formatDate(entry.publishedAt);
|
|
249
|
+
const date = formatDate(entry.publishedAt, timeZone);
|
|
250
250
|
const tags = ((_entry_tags = entry.tags) != null ? _entry_tags : []).filter(Boolean);
|
|
251
251
|
// Category name resolves against the collection's taxonomy (AGL-582):
|
|
252
252
|
// `categoryId` lookup first, legacy free-typed string fallback.
|
|
@@ -614,7 +614,7 @@ const typography = (suffix, props)=>[
|
|
|
614
614
|
}
|
|
615
615
|
/** Entry vs list fallback selection for the routed content (AGL-551). */ export function buildCollectionFallbackNodes(content) {
|
|
616
616
|
var _content_pagination, _content_category;
|
|
617
|
-
return content.entry ? buildCollectionEntryFallbackNodes(content.collection, content.entry, content.hostId) : buildCollectionListFallbackNodes(content.collection, content.entries.length > 0, (_content_pagination = content.pagination) != null ? _content_pagination : undefined, (_content_category = content.category) != null ? _content_category : undefined);
|
|
617
|
+
return content.entry ? buildCollectionEntryFallbackNodes(content.collection, content.entry, content.hostId, content.timeZone) : buildCollectionListFallbackNodes(content.collection, content.entries.length > 0, (_content_pagination = content.pagination) != null ? _content_pagination : undefined, (_content_category = content.category) != null ? _content_category : undefined);
|
|
618
618
|
}
|
|
619
619
|
export default buildCollectionFallbackNodes;
|
|
620
620
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/collection-fallback-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { wistiaMediaId } from '@aglyn/aglyn/app-utils/wistia-embed'\nimport * as Aglyn from '@aglyn/aglyn/server'\n\n/** Namespaces the synthetic fallback node ids (never persisted). */\nconst FALLBACK_ID_PREFIX = 'cfb__'\n\nconst id = (suffix: string) => `${FALLBACK_ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\ninterface FallbackCollection {\n slug: string\n displayName: string\n /** Category taxonomy (AGL-582) for `categoryId` → name resolution. */\n categories?: Aglyn.CollectionCategory[]\n}\n\ninterface FallbackEntry {\n title?: string\n slug?: string\n excerpt?: string\n /** Per-entry byline (AGL-686); falls back to the site as author. */\n authorName?: string\n body?: string\n coverImage?: string\n /** The featured video (AGL-2956); plays in the cover's place. */\n coverVideo?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /** Stable taxonomy reference (AGL-582); wins over `category`. */\n categoryId?: string\n /** Legacy free-typed category (AGL-582); read-only fallback. */\n category?: string\n tags?: string[]\n publishedAt?: { seconds: number } | null\n}\n\n// The one shared formatter (AGL-1926). This composes server-side, so the\n// inline `toLocaleDateString()` it replaces was not itself a hydration\n// mismatch — but it was a THIRD spelling of \"the entry's published date\",\n// free to drift from the byline and the related-post card that quote the\n// same instant. Pinned locale and zone, so all three agree by construction.\nconst formatDate = (value?: { seconds: number } | null) =>\n Aglyn.formatCollectionEntryDate(value)\n\n/**\n * What may be interpolated into the cover block's CSS `url(\"…\")` (AGL-1407).\n *\n * The gate this replaces was `/^https?:\\/\\//i`, which answered the safety\n * question correctly and the coverage question wrongly: it admitted only the\n * OLDEST of the stored generations. A `media:` reference failed it and the\n * cover block was never emitted — a SILENT drop, no error, no placeholder,\n * just a page missing its picture — and so did the AGL-175 relative CDN path\n * that the first pass at media references wrote. Resolving before testing is\n * what closes that, and the test then has to accept a site-relative path,\n * which is what {@link Aglyn.resolveMediaSrc} returns for a reference.\n *\n * Still a scheme allowlist rather than \"anything non-empty\": this value lands\n * inside a stylesheet rather than in an `<img src>`, so `data:` and friends\n * have no business here even though {@link cssUrlValue} already makes\n * quote-breaking impossible. A bare relative `cover.png` stays rejected\n * exactly as before — there is no page for it to resolve against at compose\n * time.\n *\n * `http:` was dropped for AGL-1725. AGL-1701 found two paths accepting it;\n * markdown-lite is AGL-1713 and this was the second, separate one. It is the\n * only scheme change here, and it is not a host restriction: an author\n * hotlinking their own https cover keeps working, because on a published\n * site the author IS the site owner and the audience is their own visitors.\n * `http:` alone has no defensible use — the request is mixed passive content\n * that current browsers already block or auto-upgrade, so the author gains\n * nothing from it, while the plaintext fetch discloses which article a\n * visitor is reading to every observer on the network path.\n *\n * The consequence of the drop is the SILENT DROP this whole comment is about,\n * for `http:` covers only: no cover node is emitted. That is the correct\n * trade here and the browser was very likely refusing the image anyway, but\n * it is the reason the change is one scheme and not a host list.\n */\nconst RENDERABLE_COVER_URL = /^(?:https:\\/\\/|\\/)/i\n\n/**\n * A URL made safe to sit inside `url(\"…\")`, WITHOUT re-encoding it.\n *\n * This replaces `encodeURI`, which was the wrong tool and quietly broke the\n * commonest stored form: `encodeURI` escapes `%`, so every raw firebasestorage\n * download URL — whose object path is `orgs%2F…%2Fmedia%2F…` — came out\n * double-encoded as `%252F` and 404'd. The block rendered, the image did not,\n * which is why it survived: the page looks composed and the picture is just\n * missing.\n *\n * Only the characters that could END the CSS string are escaped, and they are\n * escaped percent-wise rather than with a backslash so the result is still a\n * valid URL if anything downstream reads it as one. `encodeURI` was never\n * needed for anything else here — a stored URL is already URL-encoded.\n */\nconst cssUrlValue = (url: string) =>\n url.replace(/[\"\\\\\\n\\r]/g, (char) => encodeURIComponent(char))\n\n/**\n * Container width for the built-in entry article: the PROSE case of the\n * Container standard (AGL-1298), not an arbitrary narrow default. A\n * collection entry is an article body, and `xl` — the section default — runs\n * a paragraph past 180 characters a line. Stock breakpoint, no bespoke\n * number.\n */\nconst ENTRY_MAX_WIDTH = 'md'\n\n/**\n * Container width for the built-in listing: the section default. A listing is\n * a card grid rather than a reading column, so the entry's prose width is the\n * wrong measure for it, and the authored list screens render at this same\n * `xl` (AGL-2567). Still a stock breakpoint, so the AGL-1298 rule holds.\n */\nconst LIST_MAX_WIDTH = 'xl'\n\n/**\n * Vertical rhythm between the built-in listing's sections, in MUI theme\n * spacing units: `theme.spacing(6)` resolves to 48px, the same rhythm the\n * authored list screens carry, so a generated listing and an authored one\n * read as one product (AGL-2567).\n *\n * A theme token rather than a pixel string: a hand-written length here\n * answers to no theme, and a length missing its unit is a valid-looking\n * value the browser drops.\n *\n * @see https://mui.com/material-ui/customization/spacing/\n */\nconst LIST_SECTION_GAP = 6\n\n/** The page shell's width and the rhythm of the stack that holds sections. */\ninterface ShellLayout {\n /** Stock Container breakpoint (AGL-1298); never a bespoke pixel cap. */\n maxWidth: typeof ENTRY_MAX_WIDTH | typeof LIST_MAX_WIDTH\n /** Props for the content stack the page's sections slot into. */\n stackProps: Record<string, unknown>\n}\n\n/** The entry article's shell: reading column, sections close together. */\nconst entryShell = (): ShellLayout => ({\n maxWidth: ENTRY_MAX_WIDTH,\n stackProps: { spacing: 2 },\n})\n\n/**\n * The listing's shell: full section width, sections on the 48px rhythm.\n *\n * The gap is declared on the stack's `sx` rather than left to the Stack's own\n * `spacing`, which is the shape the authored list screens carry: one\n * `row-gap` on the flex container, so every section is spaced by the same\n * declaration instead of by margins that a section's own styling can collapse\n * or override.\n */\nconst listShell = (): ShellLayout => ({\n maxWidth: LIST_MAX_WIDTH,\n stackProps: {\n sx: {\n display: 'flex',\n flexDirection: 'column',\n gap: LIST_SECTION_GAP,\n },\n },\n})\n\n/** Root → centered container → content stack; children slot underneath. */\nfunction shell(childIds: string[], layout: ShellLayout): NodesMap {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n },\n [id('container')]: {\n $id: id('container'),\n componentId: 'muiContainer',\n pluginId: 'mui',\n parentId: Aglyn.NODE_ROOT_ID,\n props: {\n maxWidth: layout.maxWidth,\n sx: { paddingTop: 6, paddingBottom: 6 },\n },\n nodes: [id('stack')],\n },\n [id('stack')]: {\n $id: id('stack'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('container'),\n props: layout.stackProps,\n nodes: childIds,\n },\n }\n}\n\nconst typography = (\n suffix: string,\n props: Record<string, unknown>,\n): AglynNodeEntry => [\n id(suffix),\n {\n $id: id(suffix),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('stack'),\n props,\n },\n]\n\ntype AglynNodeEntry = [string, Aglyn.AglynNodeSchema]\n\n/**\n * The Video node's props for an entry's featured video, or `undefined` when\n * the entry has none this page can play (AGL-2956).\n *\n * An entry of a video collection is an article whose film sits where a post's\n * cover would, and on the built-in page nobody is there to bind one. So the\n * props carry what a designer would bind by hand: the source, the cover as\n * the poster, and the three facts the page's `VideoObject` is built from —\n * the title, the SEO description falling back to the excerpt as\n * `{{entry.seoDescription}}` does, and the ISO publish instant\n * `{{entry.publishedAt}}` resolves to. An instant rather than a calendar day:\n * the builder publishes `uploadDate` as a date-time with a zone, and it keeps\n * an instant as given where it would publish a bare day at noon UTC.\n *\n * `undefined` rather than a Video node in two cases, because the element's\n * answer to each is a labeled placeholder, which a published article must\n * never show:\n *\n * - a source nothing resolves, such as an empty field or a malformed `media:`\n * reference;\n * - a Wistia link with no poster. Its player loads only when a visitor\n * presses the poster, so without one there is nothing to press.\n *\n * The page then renders as a cover article, exactly as it does for an entry\n * with no video.\n *\n * The poster is the stored reference, not a resolved URL, so the element and\n * the structured data each resolve it against the site rendering the page. It\n * is passed only when the cover's own scheme rule would render it, so this\n * page never shows a still as a poster that it refuses as a cover.\n *\n * The film takes the cover's whole slot in the reading column, whose stock\n * `md` container bounds it; the column is the width limit rather than a pixel\n * cap (AGL-1298).\n */\nfunction featuredVideoProps(\n entry: FallbackEntry,\n options: { hostId?: string; posterRenders: boolean },\n): Record<string, unknown> | undefined {\n const src = (entry.coverVideo ?? '').trim()\n if (!src || !Aglyn.resolveMediaSrc(src, { hostId: options.hostId })) {\n return undefined\n }\n const poster = options.posterRenders ? entry.coverImage : undefined\n if (wistiaMediaId(src) && !poster) return undefined\n const description = entry.seoDescription || entry.excerpt || ''\n const uploadDate = Aglyn.collectionEntryPublishedAtIso(entry.publishedAt)\n return {\n src,\n ...(poster ? { poster } : {}),\n title: entry.title ?? '',\n ...(description ? { description } : {}),\n ...(uploadDate ? { uploadDate } : {}),\n width: '100%',\n }\n}\n\n/**\n * Built-in entry article as canvas nodes (AGL-551): when a collection has\n * no entry-template screen, `/{collection}/{entry}` renders these through\n * the normal compose pipeline — site theme, shared layout chrome, and the\n * markdown-rendering Entry body block — instead of the old unthemed HTML.\n */\nexport function buildCollectionEntryFallbackNodes(\n collection: FallbackCollection,\n entry: FallbackEntry,\n /**\n * The site being composed, so an org-scoped reference is host-qualified the\n * way every other render surface qualifies one (AGL-1043/AGL-1407). Optional\n * because a bare org reference still resolves without it; passing it is what\n * lets an asset restricted to this site resolve at all.\n */\n hostId?: string,\n): NodesMap {\n const entries: AglynNodeEntry[] = [\n typography('title', {\n variant: 'h3',\n component: 'h1',\n children: entry.title ?? '',\n }),\n ]\n const date = formatDate(entry.publishedAt)\n const tags = (entry.tags ?? []).filter(Boolean)\n // Category name resolves against the collection's taxonomy (AGL-582):\n // `categoryId` lookup first, legacy free-typed string fallback.\n const categoryName =\n Aglyn.resolveEntryCategoryName(entry, collection.categories) ?? ''\n if (date || categoryName || tags.length) {\n // Entry meta block (AGL-582): \"date · category\" line + tag chips.\n entries.push([\n id('meta'),\n {\n $id: id('meta'),\n componentId: Aglyn.COLLECTION_ENTRY_META_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n date,\n category: categoryName,\n tags: tags.join(', '),\n },\n },\n ])\n }\n // The cover, through the ONE shared resolver (AGL-1407). A `media:`\n // reference becomes the site-relative CDN path; a raw storage URL, an\n // AGL-175 CDN path and an author's own hotlinked URL all pass through\n // untouched, which is the documented precedence in `media-ref.ts`.\n const coverImage = Aglyn.resolveMediaSrc(entry.coverImage, { hostId })\n const coverRenders = Boolean(\n coverImage && RENDERABLE_COVER_URL.test(coverImage),\n )\n // A featured video takes the cover's slot, with the cover as its poster\n // (AGL-2956); see `featuredVideoProps` for when an entry has one to play.\n const video = featuredVideoProps(entry, {\n hostId,\n posterRenders: coverRenders,\n })\n if (video) {\n entries.push([\n id('video'),\n {\n $id: id('video'),\n componentId: Aglyn.VIDEO_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: video,\n },\n ])\n } else if (coverRenders) {\n // Rendered as a background image on a plain stack, NOT through the\n // first-party `image` component — that component crashes tenant SSR\n // (AGL-579); the background-image approach is proven safe.\n entries.push([\n id('cover'),\n {\n $id: id('cover'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n role: 'img',\n 'aria-label': entry.title ?? '',\n sx: {\n backgroundImage: `url(\"${cssUrlValue(coverImage)}\")`,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n borderRadius: 1,\n minHeight: 320,\n },\n },\n },\n ])\n }\n entries.push([\n id('body'),\n {\n $id: id('body'),\n componentId: Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { markdown: entry.body ?? '' },\n },\n ])\n // Related posts after the body, share bar at the end (AGL-582); the\n // compose pipeline stamps the related entries server-side.\n entries.push([\n id('related'),\n {\n $id: id('related'),\n componentId: Aglyn.COLLECTION_RELATED_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { heading: 'Related articles', limit: 3, sx: { paddingTop: 2 } },\n },\n ])\n entries.push([\n id('share'),\n {\n $id: id('share'),\n componentId: Aglyn.COLLECTION_SHARE_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {},\n },\n ])\n entries.push([\n id('back'),\n {\n $id: id('back'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n href: `/${collection.slug}`,\n children: `← ${collection.displayName}`,\n sx: { alignSelf: 'flex-start' },\n },\n },\n ])\n return {\n ...shell(\n entries.map(([entryId]) => entryId),\n entryShell(),\n ),\n ...Object.fromEntries(entries),\n }\n}\n\n/** Pagination state for the built-in list (AGL-620). */\nexport interface FallbackListPagination {\n page: number\n perPage: number\n /** @deprecated AGL-3219 — absent past one read's bound. */\n totalPages?: number\n /** The older page's cursor, or `''`/absent when there is none (AGL-3219). */\n nextCursor?: string\n /** The newer page's cursor. */\n prevCursor?: string\n}\n\n/** The routed category for a filtered listing (AGL-1321). */\nexport interface FallbackListCategory {\n slug: string\n name: string\n}\n\n/**\n * Prev/next nav over the cursor addresses (AGL-620, AGL-3219).\n *\n * The label says \"Page 4 of 9\" only where nine is a number that can be known\n * — a collection small enough to be read in one go. Past that it says \"Page\n * 4\" and stops, because the alternative is a total derived from a count the\n * listing's own reads could disagree with, which is how page 10 of a\n * seventeen-page changelog came to call itself the last one.\n */\nfunction paginationNodes(\n collection: FallbackCollection,\n pagination: FallbackListPagination,\n category?: FallbackListCategory,\n): { childId: string; nodes: NodesMap } {\n // Page 1 lives at the bare listing; deeper pages at .../page/{n}. Built\n // through the shared pager computation so this fallback and the\n // `{{pagination.*}}` tokens an authored template binds (AGL-1386) can never\n // disagree — and so the pager stays inside the category it is paging\n // (AGL-1321): a \"next\" that dropped the filter would silently return the\n // reader to the unfiltered list. An edge is the empty string, which is\n // exactly the \"no link here\" this fallback already meant.\n const { page, totalPages, prevUrl, nextUrl } = Aglyn.collectionPaginationLinks(\n {\n collectionSlug: collection.slug,\n ...(category ? { categorySlug: category.slug } : {}),\n page: pagination.page,\n ...(pagination.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n },\n )\n const children: string[] = []\n const nodes: NodesMap = {}\n if (prevUrl) {\n nodes[id('prev')] = {\n $id: id('prev'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: prevUrl, children: '← Newer' },\n }\n children.push(id('prev'))\n }\n nodes[id('pageinfo')] = {\n $id: id('pageinfo'),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('pager'),\n props: {\n variant: 'body2',\n children: totalPages > 1 ? `Page ${page} of ${totalPages}` : `Page ${page}`,\n sx: { color: 'text.secondary' },\n },\n }\n children.push(id('pageinfo'))\n if (nextUrl) {\n nodes[id('next')] = {\n $id: id('next'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: nextUrl, children: 'Older →' },\n }\n children.push(id('next'))\n }\n nodes[id('pager')] = {\n $id: id('pager'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n direction: 'row',\n spacing: 3,\n sx: { alignItems: 'center', justifyContent: 'center' },\n },\n nodes: children,\n }\n return { childId: id('pager'), nodes }\n}\n\n/**\n * Built-in entry list as canvas nodes (AGL-551): a heading plus a\n * Collection entries block whose template (title, date, excerpt, Read\n * more) the compose pipeline expands over the published entries — the same\n * block designers drop onto their own list-template screens. With\n * `pagination` (AGL-620) the block renders one page and prev/next nav links\n * to `/{slug}/page/{n}`.\n */\nexport function buildCollectionListFallbackNodes(\n collection: FallbackCollection,\n hasEntries: boolean,\n pagination?: FallbackListPagination,\n category?: FallbackListCategory,\n): NodesMap {\n const [titleId, title] = typography('title', {\n variant: 'h3',\n component: 'h1',\n // A filtered listing says which slice it is (AGL-1321) — a page of\n // Guides whose only heading reads \"Blog\" tells the reader nothing about\n // why the other posts vanished.\n children: category\n ? `${collection.displayName} · ${category.name}`\n : collection.displayName,\n })\n // Category pills (AGL-1321): stamped by `expandCollectionCategories` during\n // compose, from the same taxonomy this node's collection carries. Present\n // in the EMPTY case too — a filtered listing with no entries whose pills\n // vanished would be a dead end with no way back to \"All\".\n const pills = collection.categories?.length\n ? {\n $id: id('pills'),\n componentId: Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { allLabel: 'All' },\n }\n : null\n const lead = pills ? [titleId, id('pills')] : [titleId]\n if (!hasEntries) {\n const [emptyId, empty] = typography('empty', {\n variant: 'body1',\n children: category\n ? `Nothing published in ${category.name} yet.`\n : 'Nothing published yet.',\n sx: { color: 'text.secondary' },\n })\n return {\n ...shell([...lead, emptyId], listShell()),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [emptyId]: empty,\n }\n }\n const item = (suffix: string, props: Record<string, unknown>) => ({\n $id: id(suffix),\n componentId: 'muiTypography' as const,\n pluginId: 'mui',\n parentId: id('item'),\n props,\n })\n /*\n * A pager exists when there is somewhere to go (AGL-3219): an older page,\n * proved by the cursor probe, or a newer one, proved by being past the\n * first. `totalPages > 1` used to stand in for that and could not, once the\n * total stopped being knowable.\n */\n const pager =\n pagination &&\n (Boolean(pagination.nextCursor) ||\n pagination.page > 1 ||\n (pagination.totalPages ?? 0) > 1)\n ? paginationNodes(collection, pagination, category)\n : null\n return {\n ...shell(\n pager\n ? [...lead, id('entries'), pager.childId]\n : [...lead, id('entries')],\n listShell(),\n ),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [id('entries')]: {\n $id: id('entries'),\n componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n spacing: 4,\n ...(pagination\n ? { perPage: pagination.perPage, page: pagination.page }\n : {}),\n },\n nodes: [id('item')],\n },\n ...(pager ? pager.nodes : {}),\n [id('item')]: {\n $id: id('item'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('entries'),\n props: { spacing: 0.5 },\n nodes: [\n id('item-title'),\n id('item-date'),\n id('item-excerpt'),\n id('item-link'),\n ],\n },\n [id('item-title')]: item('item-title', {\n variant: 'h5',\n component: 'h2',\n children: '{{entry.title}}',\n }),\n [id('item-date')]: item('item-date', {\n variant: 'caption',\n children: '{{entry.date}}',\n sx: { color: 'text.secondary' },\n }),\n [id('item-excerpt')]: item('item-excerpt', {\n variant: 'body1',\n children: '{{entry.excerpt}}',\n }),\n [id('item-link')]: {\n $id: id('item-link'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('item'),\n props: {\n href: '{{entry.url}}',\n children: 'Read more',\n size: 'small',\n sx: { alignSelf: 'flex-start' },\n },\n },\n }\n}\n\n/** Entry vs list fallback selection for the routed content (AGL-551). */\nexport function buildCollectionFallbackNodes(content: {\n collection: FallbackCollection\n entries: FallbackEntry[]\n entry: FallbackEntry | null\n pagination?: FallbackListPagination | null\n category?: FallbackListCategory | null\n /** The composing site, for host-qualifying a media reference (AGL-1407). */\n hostId?: string\n}): NodesMap {\n return content.entry\n ? buildCollectionEntryFallbackNodes(\n content.collection,\n content.entry,\n content.hostId,\n )\n : buildCollectionListFallbackNodes(\n content.collection,\n content.entries.length > 0,\n content.pagination ?? undefined,\n content.category ?? undefined,\n )\n}\n\nexport default buildCollectionFallbackNodes\n"],"names":["wistiaMediaId","Aglyn","FALLBACK_ID_PREFIX","id","suffix","formatDate","value","formatCollectionEntryDate","RENDERABLE_COVER_URL","cssUrlValue","url","replace","char","encodeURIComponent","ENTRY_MAX_WIDTH","LIST_MAX_WIDTH","LIST_SECTION_GAP","entryShell","maxWidth","stackProps","spacing","listShell","sx","display","flexDirection","gap","shell","childIds","layout","NODE_ROOT_ID","$id","componentId","nodes","pluginId","parentId","props","paddingTop","paddingBottom","typography","featuredVideoProps","entry","options","src","coverVideo","trim","resolveMediaSrc","hostId","undefined","poster","posterRenders","coverImage","description","seoDescription","excerpt","uploadDate","collectionEntryPublishedAtIso","publishedAt","title","width","buildCollectionEntryFallbackNodes","collection","entries","variant","component","children","date","tags","filter","Boolean","categoryName","resolveEntryCategoryName","categories","length","push","COLLECTION_ENTRY_META_COMPONENT_ID","category","join","coverRenders","test","video","VIDEO_COMPONENT_ID","role","backgroundImage","backgroundSize","backgroundPosition","borderRadius","minHeight","COLLECTION_ENTRY_BODY_COMPONENT_ID","markdown","body","COLLECTION_RELATED_COMPONENT_ID","heading","limit","COLLECTION_SHARE_COMPONENT_ID","href","slug","displayName","alignSelf","map","entryId","Object","fromEntries","paginationNodes","pagination","page","totalPages","prevUrl","nextUrl","collectionPaginationLinks","collectionSlug","categorySlug","nextCursor","prevCursor","color","direction","alignItems","justifyContent","childId","buildCollectionListFallbackNodes","hasEntries","titleId","name","pills","COLLECTION_CATEGORIES_COMPONENT_ID","allLabel","lead","emptyId","empty","item","pager","COLLECTION_ENTRIES_COMPONENT_ID","perPage","size","buildCollectionFallbackNodes","content"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/collection-fallback-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { wistiaMediaId } from '@aglyn/aglyn/app-utils/wistia-embed'\nimport * as Aglyn from '@aglyn/aglyn/server'\n\n/** Namespaces the synthetic fallback node ids (never persisted). */\nconst FALLBACK_ID_PREFIX = 'cfb__'\n\nconst id = (suffix: string) => `${FALLBACK_ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\ninterface FallbackCollection {\n slug: string\n displayName: string\n /** Category taxonomy (AGL-582) for `categoryId` → name resolution. */\n categories?: Aglyn.CollectionCategory[]\n}\n\ninterface FallbackEntry {\n title?: string\n slug?: string\n excerpt?: string\n /** Per-entry byline (AGL-686); falls back to the site as author. */\n authorName?: string\n body?: string\n coverImage?: string\n /** The featured video (AGL-2956); plays in the cover's place. */\n coverVideo?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /** Stable taxonomy reference (AGL-582); wins over `category`. */\n categoryId?: string\n /** Legacy free-typed category (AGL-582); read-only fallback. */\n category?: string\n tags?: string[]\n publishedAt?: { seconds: number } | null\n}\n\n// The one shared formatter (AGL-1926). This composes server-side, so the\n// inline `toLocaleDateString()` it replaces was not itself a hydration\n// mismatch — but it was a THIRD spelling of \"the entry's published date\",\n// free to drift from the byline and the related-post card that quote the\n// same instant. Pinned locale and zone, so all three agree by construction.\nconst formatDate = (value?: { seconds: number } | null, timeZone?: string) =>\n Aglyn.formatCollectionEntryDate(value, undefined, undefined, timeZone)\n\n/**\n * What may be interpolated into the cover block's CSS `url(\"…\")` (AGL-1407).\n *\n * The gate this replaces was `/^https?:\\/\\//i`, which answered the safety\n * question correctly and the coverage question wrongly: it admitted only the\n * OLDEST of the stored generations. A `media:` reference failed it and the\n * cover block was never emitted — a SILENT drop, no error, no placeholder,\n * just a page missing its picture — and so did the AGL-175 relative CDN path\n * that the first pass at media references wrote. Resolving before testing is\n * what closes that, and the test then has to accept a site-relative path,\n * which is what {@link Aglyn.resolveMediaSrc} returns for a reference.\n *\n * Still a scheme allowlist rather than \"anything non-empty\": this value lands\n * inside a stylesheet rather than in an `<img src>`, so `data:` and friends\n * have no business here even though {@link cssUrlValue} already makes\n * quote-breaking impossible. A bare relative `cover.png` stays rejected\n * exactly as before — there is no page for it to resolve against at compose\n * time.\n *\n * `http:` was dropped for AGL-1725. AGL-1701 found two paths accepting it;\n * markdown-lite is AGL-1713 and this was the second, separate one. It is the\n * only scheme change here, and it is not a host restriction: an author\n * hotlinking their own https cover keeps working, because on a published\n * site the author IS the site owner and the audience is their own visitors.\n * `http:` alone has no defensible use — the request is mixed passive content\n * that current browsers already block or auto-upgrade, so the author gains\n * nothing from it, while the plaintext fetch discloses which article a\n * visitor is reading to every observer on the network path.\n *\n * The consequence of the drop is the SILENT DROP this whole comment is about,\n * for `http:` covers only: no cover node is emitted. That is the correct\n * trade here and the browser was very likely refusing the image anyway, but\n * it is the reason the change is one scheme and not a host list.\n */\nconst RENDERABLE_COVER_URL = /^(?:https:\\/\\/|\\/)/i\n\n/**\n * A URL made safe to sit inside `url(\"…\")`, WITHOUT re-encoding it.\n *\n * This replaces `encodeURI`, which was the wrong tool and quietly broke the\n * commonest stored form: `encodeURI` escapes `%`, so every raw firebasestorage\n * download URL — whose object path is `orgs%2F…%2Fmedia%2F…` — came out\n * double-encoded as `%252F` and 404'd. The block rendered, the image did not,\n * which is why it survived: the page looks composed and the picture is just\n * missing.\n *\n * Only the characters that could END the CSS string are escaped, and they are\n * escaped percent-wise rather than with a backslash so the result is still a\n * valid URL if anything downstream reads it as one. `encodeURI` was never\n * needed for anything else here — a stored URL is already URL-encoded.\n */\nconst cssUrlValue = (url: string) =>\n url.replace(/[\"\\\\\\n\\r]/g, (char) => encodeURIComponent(char))\n\n/**\n * Container width for the built-in entry article: the PROSE case of the\n * Container standard (AGL-1298), not an arbitrary narrow default. A\n * collection entry is an article body, and `xl` — the section default — runs\n * a paragraph past 180 characters a line. Stock breakpoint, no bespoke\n * number.\n */\nconst ENTRY_MAX_WIDTH = 'md'\n\n/**\n * Container width for the built-in listing: the section default. A listing is\n * a card grid rather than a reading column, so the entry's prose width is the\n * wrong measure for it, and the authored list screens render at this same\n * `xl` (AGL-2567). Still a stock breakpoint, so the AGL-1298 rule holds.\n */\nconst LIST_MAX_WIDTH = 'xl'\n\n/**\n * Vertical rhythm between the built-in listing's sections, in MUI theme\n * spacing units: `theme.spacing(6)` resolves to 48px, the same rhythm the\n * authored list screens carry, so a generated listing and an authored one\n * read as one product (AGL-2567).\n *\n * A theme token rather than a pixel string: a hand-written length here\n * answers to no theme, and a length missing its unit is a valid-looking\n * value the browser drops.\n *\n * @see https://mui.com/material-ui/customization/spacing/\n */\nconst LIST_SECTION_GAP = 6\n\n/** The page shell's width and the rhythm of the stack that holds sections. */\ninterface ShellLayout {\n /** Stock Container breakpoint (AGL-1298); never a bespoke pixel cap. */\n maxWidth: typeof ENTRY_MAX_WIDTH | typeof LIST_MAX_WIDTH\n /** Props for the content stack the page's sections slot into. */\n stackProps: Record<string, unknown>\n}\n\n/** The entry article's shell: reading column, sections close together. */\nconst entryShell = (): ShellLayout => ({\n maxWidth: ENTRY_MAX_WIDTH,\n stackProps: { spacing: 2 },\n})\n\n/**\n * The listing's shell: full section width, sections on the 48px rhythm.\n *\n * The gap is declared on the stack's `sx` rather than left to the Stack's own\n * `spacing`, which is the shape the authored list screens carry: one\n * `row-gap` on the flex container, so every section is spaced by the same\n * declaration instead of by margins that a section's own styling can collapse\n * or override.\n */\nconst listShell = (): ShellLayout => ({\n maxWidth: LIST_MAX_WIDTH,\n stackProps: {\n sx: {\n display: 'flex',\n flexDirection: 'column',\n gap: LIST_SECTION_GAP,\n },\n },\n})\n\n/** Root → centered container → content stack; children slot underneath. */\nfunction shell(childIds: string[], layout: ShellLayout): NodesMap {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n },\n [id('container')]: {\n $id: id('container'),\n componentId: 'muiContainer',\n pluginId: 'mui',\n parentId: Aglyn.NODE_ROOT_ID,\n props: {\n maxWidth: layout.maxWidth,\n sx: { paddingTop: 6, paddingBottom: 6 },\n },\n nodes: [id('stack')],\n },\n [id('stack')]: {\n $id: id('stack'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('container'),\n props: layout.stackProps,\n nodes: childIds,\n },\n }\n}\n\nconst typography = (\n suffix: string,\n props: Record<string, unknown>,\n): AglynNodeEntry => [\n id(suffix),\n {\n $id: id(suffix),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('stack'),\n props,\n },\n]\n\ntype AglynNodeEntry = [string, Aglyn.AglynNodeSchema]\n\n/**\n * The Video node's props for an entry's featured video, or `undefined` when\n * the entry has none this page can play (AGL-2956).\n *\n * An entry of a video collection is an article whose film sits where a post's\n * cover would, and on the built-in page nobody is there to bind one. So the\n * props carry what a designer would bind by hand: the source, the cover as\n * the poster, and the three facts the page's `VideoObject` is built from —\n * the title, the SEO description falling back to the excerpt as\n * `{{entry.seoDescription}}` does, and the ISO publish instant\n * `{{entry.publishedAt}}` resolves to. An instant rather than a calendar day:\n * the builder publishes `uploadDate` as a date-time with a zone, and it keeps\n * an instant as given where it would publish a bare day at noon UTC.\n *\n * `undefined` rather than a Video node in two cases, because the element's\n * answer to each is a labeled placeholder, which a published article must\n * never show:\n *\n * - a source nothing resolves, such as an empty field or a malformed `media:`\n * reference;\n * - a Wistia link with no poster. Its player loads only when a visitor\n * presses the poster, so without one there is nothing to press.\n *\n * The page then renders as a cover article, exactly as it does for an entry\n * with no video.\n *\n * The poster is the stored reference, not a resolved URL, so the element and\n * the structured data each resolve it against the site rendering the page. It\n * is passed only when the cover's own scheme rule would render it, so this\n * page never shows a still as a poster that it refuses as a cover.\n *\n * The film takes the cover's whole slot in the reading column, whose stock\n * `md` container bounds it; the column is the width limit rather than a pixel\n * cap (AGL-1298).\n */\nfunction featuredVideoProps(\n entry: FallbackEntry,\n options: { hostId?: string; posterRenders: boolean },\n): Record<string, unknown> | undefined {\n const src = (entry.coverVideo ?? '').trim()\n if (!src || !Aglyn.resolveMediaSrc(src, { hostId: options.hostId })) {\n return undefined\n }\n const poster = options.posterRenders ? entry.coverImage : undefined\n if (wistiaMediaId(src) && !poster) return undefined\n const description = entry.seoDescription || entry.excerpt || ''\n const uploadDate = Aglyn.collectionEntryPublishedAtIso(entry.publishedAt)\n return {\n src,\n ...(poster ? { poster } : {}),\n title: entry.title ?? '',\n ...(description ? { description } : {}),\n ...(uploadDate ? { uploadDate } : {}),\n width: '100%',\n }\n}\n\n/**\n * Built-in entry article as canvas nodes (AGL-551): when a collection has\n * no entry-template screen, `/{collection}/{entry}` renders these through\n * the normal compose pipeline — site theme, shared layout chrome, and the\n * markdown-rendering Entry body block — instead of the old unthemed HTML.\n */\nexport function buildCollectionEntryFallbackNodes(\n collection: FallbackCollection,\n entry: FallbackEntry,\n /**\n * The site being composed, so an org-scoped reference is host-qualified the\n * way every other render surface qualifies one (AGL-1043/AGL-1407). Optional\n * because a bare org reference still resolves without it; passing it is what\n * lets an asset restricted to this site resolve at all.\n */\n hostId?: string,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): NodesMap {\n const entries: AglynNodeEntry[] = [\n typography('title', {\n variant: 'h3',\n component: 'h1',\n children: entry.title ?? '',\n }),\n ]\n const date = formatDate(entry.publishedAt, timeZone)\n const tags = (entry.tags ?? []).filter(Boolean)\n // Category name resolves against the collection's taxonomy (AGL-582):\n // `categoryId` lookup first, legacy free-typed string fallback.\n const categoryName =\n Aglyn.resolveEntryCategoryName(entry, collection.categories) ?? ''\n if (date || categoryName || tags.length) {\n // Entry meta block (AGL-582): \"date · category\" line + tag chips.\n entries.push([\n id('meta'),\n {\n $id: id('meta'),\n componentId: Aglyn.COLLECTION_ENTRY_META_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n date,\n category: categoryName,\n tags: tags.join(', '),\n },\n },\n ])\n }\n // The cover, through the ONE shared resolver (AGL-1407). A `media:`\n // reference becomes the site-relative CDN path; a raw storage URL, an\n // AGL-175 CDN path and an author's own hotlinked URL all pass through\n // untouched, which is the documented precedence in `media-ref.ts`.\n const coverImage = Aglyn.resolveMediaSrc(entry.coverImage, { hostId })\n const coverRenders = Boolean(\n coverImage && RENDERABLE_COVER_URL.test(coverImage),\n )\n // A featured video takes the cover's slot, with the cover as its poster\n // (AGL-2956); see `featuredVideoProps` for when an entry has one to play.\n const video = featuredVideoProps(entry, {\n hostId,\n posterRenders: coverRenders,\n })\n if (video) {\n entries.push([\n id('video'),\n {\n $id: id('video'),\n componentId: Aglyn.VIDEO_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: video,\n },\n ])\n } else if (coverRenders) {\n // Rendered as a background image on a plain stack, NOT through the\n // first-party `image` component — that component crashes tenant SSR\n // (AGL-579); the background-image approach is proven safe.\n entries.push([\n id('cover'),\n {\n $id: id('cover'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n role: 'img',\n 'aria-label': entry.title ?? '',\n sx: {\n backgroundImage: `url(\"${cssUrlValue(coverImage)}\")`,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n borderRadius: 1,\n minHeight: 320,\n },\n },\n },\n ])\n }\n entries.push([\n id('body'),\n {\n $id: id('body'),\n componentId: Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { markdown: entry.body ?? '' },\n },\n ])\n // Related posts after the body, share bar at the end (AGL-582); the\n // compose pipeline stamps the related entries server-side.\n entries.push([\n id('related'),\n {\n $id: id('related'),\n componentId: Aglyn.COLLECTION_RELATED_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { heading: 'Related articles', limit: 3, sx: { paddingTop: 2 } },\n },\n ])\n entries.push([\n id('share'),\n {\n $id: id('share'),\n componentId: Aglyn.COLLECTION_SHARE_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {},\n },\n ])\n entries.push([\n id('back'),\n {\n $id: id('back'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n href: `/${collection.slug}`,\n children: `← ${collection.displayName}`,\n sx: { alignSelf: 'flex-start' },\n },\n },\n ])\n return {\n ...shell(\n entries.map(([entryId]) => entryId),\n entryShell(),\n ),\n ...Object.fromEntries(entries),\n }\n}\n\n/** Pagination state for the built-in list (AGL-620). */\nexport interface FallbackListPagination {\n page: number\n perPage: number\n /** @deprecated AGL-3219 — absent past one read's bound. */\n totalPages?: number\n /** The older page's cursor, or `''`/absent when there is none (AGL-3219). */\n nextCursor?: string\n /** The newer page's cursor. */\n prevCursor?: string\n}\n\n/** The routed category for a filtered listing (AGL-1321). */\nexport interface FallbackListCategory {\n slug: string\n name: string\n}\n\n/**\n * Prev/next nav over the cursor addresses (AGL-620, AGL-3219).\n *\n * The label says \"Page 4 of 9\" only where nine is a number that can be known\n * — a collection small enough to be read in one go. Past that it says \"Page\n * 4\" and stops, because the alternative is a total derived from a count the\n * listing's own reads could disagree with, which is how page 10 of a\n * seventeen-page changelog came to call itself the last one.\n */\nfunction paginationNodes(\n collection: FallbackCollection,\n pagination: FallbackListPagination,\n category?: FallbackListCategory,\n): { childId: string; nodes: NodesMap } {\n // Page 1 lives at the bare listing; deeper pages at .../page/{n}. Built\n // through the shared pager computation so this fallback and the\n // `{{pagination.*}}` tokens an authored template binds (AGL-1386) can never\n // disagree — and so the pager stays inside the category it is paging\n // (AGL-1321): a \"next\" that dropped the filter would silently return the\n // reader to the unfiltered list. An edge is the empty string, which is\n // exactly the \"no link here\" this fallback already meant.\n const { page, totalPages, prevUrl, nextUrl } = Aglyn.collectionPaginationLinks(\n {\n collectionSlug: collection.slug,\n ...(category ? { categorySlug: category.slug } : {}),\n page: pagination.page,\n ...(pagination.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n },\n )\n const children: string[] = []\n const nodes: NodesMap = {}\n if (prevUrl) {\n nodes[id('prev')] = {\n $id: id('prev'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: prevUrl, children: '← Newer' },\n }\n children.push(id('prev'))\n }\n nodes[id('pageinfo')] = {\n $id: id('pageinfo'),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('pager'),\n props: {\n variant: 'body2',\n children: totalPages > 1 ? `Page ${page} of ${totalPages}` : `Page ${page}`,\n sx: { color: 'text.secondary' },\n },\n }\n children.push(id('pageinfo'))\n if (nextUrl) {\n nodes[id('next')] = {\n $id: id('next'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: nextUrl, children: 'Older →' },\n }\n children.push(id('next'))\n }\n nodes[id('pager')] = {\n $id: id('pager'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n direction: 'row',\n spacing: 3,\n sx: { alignItems: 'center', justifyContent: 'center' },\n },\n nodes: children,\n }\n return { childId: id('pager'), nodes }\n}\n\n/**\n * Built-in entry list as canvas nodes (AGL-551): a heading plus a\n * Collection entries block whose template (title, date, excerpt, Read\n * more) the compose pipeline expands over the published entries — the same\n * block designers drop onto their own list-template screens. With\n * `pagination` (AGL-620) the block renders one page and prev/next nav links\n * to `/{slug}/page/{n}`.\n */\nexport function buildCollectionListFallbackNodes(\n collection: FallbackCollection,\n hasEntries: boolean,\n pagination?: FallbackListPagination,\n category?: FallbackListCategory,\n): NodesMap {\n const [titleId, title] = typography('title', {\n variant: 'h3',\n component: 'h1',\n // A filtered listing says which slice it is (AGL-1321) — a page of\n // Guides whose only heading reads \"Blog\" tells the reader nothing about\n // why the other posts vanished.\n children: category\n ? `${collection.displayName} · ${category.name}`\n : collection.displayName,\n })\n // Category pills (AGL-1321): stamped by `expandCollectionCategories` during\n // compose, from the same taxonomy this node's collection carries. Present\n // in the EMPTY case too — a filtered listing with no entries whose pills\n // vanished would be a dead end with no way back to \"All\".\n const pills = collection.categories?.length\n ? {\n $id: id('pills'),\n componentId: Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { allLabel: 'All' },\n }\n : null\n const lead = pills ? [titleId, id('pills')] : [titleId]\n if (!hasEntries) {\n const [emptyId, empty] = typography('empty', {\n variant: 'body1',\n children: category\n ? `Nothing published in ${category.name} yet.`\n : 'Nothing published yet.',\n sx: { color: 'text.secondary' },\n })\n return {\n ...shell([...lead, emptyId], listShell()),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [emptyId]: empty,\n }\n }\n const item = (suffix: string, props: Record<string, unknown>) => ({\n $id: id(suffix),\n componentId: 'muiTypography' as const,\n pluginId: 'mui',\n parentId: id('item'),\n props,\n })\n /*\n * A pager exists when there is somewhere to go (AGL-3219): an older page,\n * proved by the cursor probe, or a newer one, proved by being past the\n * first. `totalPages > 1` used to stand in for that and could not, once the\n * total stopped being knowable.\n */\n const pager =\n pagination &&\n (Boolean(pagination.nextCursor) ||\n pagination.page > 1 ||\n (pagination.totalPages ?? 0) > 1)\n ? paginationNodes(collection, pagination, category)\n : null\n return {\n ...shell(\n pager\n ? [...lead, id('entries'), pager.childId]\n : [...lead, id('entries')],\n listShell(),\n ),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [id('entries')]: {\n $id: id('entries'),\n componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n spacing: 4,\n ...(pagination\n ? { perPage: pagination.perPage, page: pagination.page }\n : {}),\n },\n nodes: [id('item')],\n },\n ...(pager ? pager.nodes : {}),\n [id('item')]: {\n $id: id('item'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('entries'),\n props: { spacing: 0.5 },\n nodes: [\n id('item-title'),\n id('item-date'),\n id('item-excerpt'),\n id('item-link'),\n ],\n },\n [id('item-title')]: item('item-title', {\n variant: 'h5',\n component: 'h2',\n children: '{{entry.title}}',\n }),\n [id('item-date')]: item('item-date', {\n variant: 'caption',\n children: '{{entry.date}}',\n sx: { color: 'text.secondary' },\n }),\n [id('item-excerpt')]: item('item-excerpt', {\n variant: 'body1',\n children: '{{entry.excerpt}}',\n }),\n [id('item-link')]: {\n $id: id('item-link'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('item'),\n props: {\n href: '{{entry.url}}',\n children: 'Read more',\n size: 'small',\n sx: { alignSelf: 'flex-start' },\n },\n },\n }\n}\n\n/** Entry vs list fallback selection for the routed content (AGL-551). */\nexport function buildCollectionFallbackNodes(content: {\n collection: FallbackCollection\n entries: FallbackEntry[]\n entry: FallbackEntry | null\n pagination?: FallbackListPagination | null\n category?: FallbackListCategory | null\n /** The composing site, for host-qualifying a media reference (AGL-1407). */\n hostId?: string\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string\n}): NodesMap {\n return content.entry\n ? buildCollectionEntryFallbackNodes(\n content.collection,\n content.entry,\n content.hostId,\n content.timeZone,\n )\n : buildCollectionListFallbackNodes(\n content.collection,\n content.entries.length > 0,\n content.pagination ?? undefined,\n content.category ?? undefined,\n )\n}\n\nexport default buildCollectionFallbackNodes\n"],"names":["wistiaMediaId","Aglyn","FALLBACK_ID_PREFIX","id","suffix","formatDate","value","timeZone","formatCollectionEntryDate","undefined","RENDERABLE_COVER_URL","cssUrlValue","url","replace","char","encodeURIComponent","ENTRY_MAX_WIDTH","LIST_MAX_WIDTH","LIST_SECTION_GAP","entryShell","maxWidth","stackProps","spacing","listShell","sx","display","flexDirection","gap","shell","childIds","layout","NODE_ROOT_ID","$id","componentId","nodes","pluginId","parentId","props","paddingTop","paddingBottom","typography","featuredVideoProps","entry","options","src","coverVideo","trim","resolveMediaSrc","hostId","poster","posterRenders","coverImage","description","seoDescription","excerpt","uploadDate","collectionEntryPublishedAtIso","publishedAt","title","width","buildCollectionEntryFallbackNodes","collection","entries","variant","component","children","date","tags","filter","Boolean","categoryName","resolveEntryCategoryName","categories","length","push","COLLECTION_ENTRY_META_COMPONENT_ID","category","join","coverRenders","test","video","VIDEO_COMPONENT_ID","role","backgroundImage","backgroundSize","backgroundPosition","borderRadius","minHeight","COLLECTION_ENTRY_BODY_COMPONENT_ID","markdown","body","COLLECTION_RELATED_COMPONENT_ID","heading","limit","COLLECTION_SHARE_COMPONENT_ID","href","slug","displayName","alignSelf","map","entryId","Object","fromEntries","paginationNodes","pagination","page","totalPages","prevUrl","nextUrl","collectionPaginationLinks","collectionSlug","categorySlug","nextCursor","prevCursor","color","direction","alignItems","justifyContent","childId","buildCollectionListFallbackNodes","hasEntries","titleId","name","pills","COLLECTION_CATEGORIES_COMPONENT_ID","allLabel","lead","emptyId","empty","item","pager","COLLECTION_ENTRIES_COMPONENT_ID","perPage","size","buildCollectionFallbackNodes","content"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,sCAAqC;AACnE,YAAYC,WAAW,sBAAqB;AAE5C,kEAAkE,GAClE,MAAMC,qBAAqB;AAE3B,MAAMC,KAAK,CAACC,SAAmB,GAAGF,qBAAqBE,QAAQ;AA+B/D,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,yEAAyE;AACzE,4EAA4E;AAC5E,MAAMC,aAAa,CAACC,OAAoCC,WACtDN,MAAMO,yBAAyB,CAACF,OAAOG,WAAWA,WAAWF;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCC,GACD,MAAMG,uBAAuB;AAE7B;;;;;;;;;;;;;;CAcC,GACD,MAAMC,cAAc,CAACC,MACnBA,IAAIC,OAAO,CAAC,cAAc,CAACC,OAASC,mBAAmBD;AAEzD;;;;;;CAMC,GACD,MAAME,kBAAkB;AAExB;;;;;CAKC,GACD,MAAMC,iBAAiB;AAEvB;;;;;;;;;;;CAWC,GACD,MAAMC,mBAAmB;AAUzB,wEAAwE,GACxE,MAAMC,aAAa,IAAoB,CAAA;QACrCC,UAAUJ;QACVK,YAAY;YAAEC,SAAS;QAAE;IAC3B,CAAA;AAEA;;;;;;;;CAQC,GACD,MAAMC,YAAY,IAAoB,CAAA;QACpCH,UAAUH;QACVI,YAAY;YACVG,IAAI;gBACFC,SAAS;gBACTC,eAAe;gBACfC,KAAKT;YACP;QACF;IACF,CAAA;AAEA,yEAAyE,GACzE,SAASU,MAAMC,QAAkB,EAAEC,MAAmB;IACpD,OAAO;QACL,CAAC7B,MAAM8B,YAAY,CAAC,EAAE;YACpBC,KAAK/B,MAAM8B,YAAY;YACvBE,aAAa;YACbC,OAAO;gBAAC/B,GAAG;aAAa;QAC1B;QACA,CAACA,GAAG,aAAa,EAAE;YACjB6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUnC,MAAM8B,YAAY;YAC5BM,OAAO;gBACLjB,UAAUU,OAAOV,QAAQ;gBACzBI,IAAI;oBAAEc,YAAY;oBAAGC,eAAe;gBAAE;YACxC;YACAL,OAAO;gBAAC/B,GAAG;aAAS;QACtB;QACA,CAACA,GAAG,SAAS,EAAE;YACb6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAOP,OAAOT,UAAU;YACxBa,OAAOL;QACT;IACF;AACF;AAEA,MAAMW,aAAa,CACjBpC,QACAiC,QACmB;QACnBlC,GAAGC;QACH;YACE4B,KAAK7B,GAAGC;YACR6B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC;QACF;KACD;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,SAASI,mBACPC,KAAoB,EACpBC,OAAoD;QAEvCD,mBAWJA;IAXT,MAAME,MAAM,EAACF,oBAAAA,MAAMG,UAAU,YAAhBH,oBAAoB,IAAII,IAAI;IACzC,IAAI,CAACF,OAAO,CAAC3C,MAAM8C,eAAe,CAACH,KAAK;QAAEI,QAAQL,QAAQK,MAAM;IAAC,IAAI;QACnE,OAAOvC;IACT;IACA,MAAMwC,SAASN,QAAQO,aAAa,GAAGR,MAAMS,UAAU,GAAG1C;IAC1D,IAAIT,cAAc4C,QAAQ,CAACK,QAAQ,OAAOxC;IAC1C,MAAM2C,cAAcV,MAAMW,cAAc,IAAIX,MAAMY,OAAO,IAAI;IAC7D,MAAMC,aAAatD,MAAMuD,6BAA6B,CAACd,MAAMe,WAAW;IACxE,OAAO;QACLb;OACIK,SAAS;QAAEA;IAAO,IAAI,CAAC;QAC3BS,KAAK,GAAEhB,eAAAA,MAAMgB,KAAK,YAAXhB,eAAe;OAClBU,cAAc;QAAEA;IAAY,IAAI,CAAC,GACjCG,aAAa;QAAEA;IAAW,IAAI,CAAC;QACnCI,OAAO;;AAEX;AAEA;;;;;CAKC,GACD,OAAO,SAASC,kCACdC,UAA8B,EAC9BnB,KAAoB,EACpB;;;;;GAKC,GACDM,MAAe,EACf,mEAAmE,GACnEzC,QAAiB;QAMHmC,cAIAA,aAIZzC,iCA2EqByC;IAvFvB,MAAMoB,UAA4B;QAChCtB,WAAW,SAAS;YAClBuB,SAAS;YACTC,WAAW;YACXC,QAAQ,GAAEvB,eAAAA,MAAMgB,KAAK,YAAXhB,eAAe;QAC3B;KACD;IACD,MAAMwB,OAAO7D,WAAWqC,MAAMe,WAAW,EAAElD;IAC3C,MAAM4D,OAAO,EAACzB,cAAAA,MAAMyB,IAAI,YAAVzB,cAAc,EAAE,EAAE0B,MAAM,CAACC;IACvC,sEAAsE;IACtE,gEAAgE;IAChE,MAAMC,gBACJrE,kCAAAA,MAAMsE,wBAAwB,CAAC7B,OAAOmB,WAAWW,UAAU,aAA3DvE,kCAAgE;IAClE,IAAIiE,QAAQI,gBAAgBH,KAAKM,MAAM,EAAE;QACvC,kEAAkE;QAClEX,QAAQY,IAAI,CAAC;YACXvE,GAAG;YACH;gBACE6B,KAAK7B,GAAG;gBACR8B,aAAahC,MAAM0E,kCAAkC;gBACrDxC,UAAU;gBACVC,UAAUjC,GAAG;gBACbkC,OAAO;oBACL6B;oBACAU,UAAUN;oBACVH,MAAMA,KAAKU,IAAI,CAAC;gBAClB;YACF;SACD;IACH;IACA,oEAAoE;IACpE,sEAAsE;IACtE,sEAAsE;IACtE,mEAAmE;IACnE,MAAM1B,aAAalD,MAAM8C,eAAe,CAACL,MAAMS,UAAU,EAAE;QAAEH;IAAO;IACpE,MAAM8B,eAAeT,QACnBlB,cAAczC,qBAAqBqE,IAAI,CAAC5B;IAE1C,wEAAwE;IACxE,0EAA0E;IAC1E,MAAM6B,QAAQvC,mBAAmBC,OAAO;QACtCM;QACAE,eAAe4B;IACjB;IACA,IAAIE,OAAO;QACTlB,QAAQY,IAAI,CAAC;YACXvE,GAAG;YACH;gBACE6B,KAAK7B,GAAG;gBACR8B,aAAahC,MAAMgF,kBAAkB;gBACrC9C,UAAU;gBACVC,UAAUjC,GAAG;gBACbkC,OAAO2C;YACT;SACD;IACH,OAAO,IAAIF,cAAc;YAaHpC;QAZpB,mEAAmE;QACnE,oEAAoE;QACpE,2DAA2D;QAC3DoB,QAAQY,IAAI,CAAC;YACXvE,GAAG;YACH;gBACE6B,KAAK7B,GAAG;gBACR8B,aAAa;gBACbE,UAAU;gBACVC,UAAUjC,GAAG;gBACbkC,OAAO;oBACL6C,MAAM;oBACN,YAAY,GAAExC,gBAAAA,MAAMgB,KAAK,YAAXhB,gBAAe;oBAC7BlB,IAAI;wBACF2D,iBAAiB,CAAC,KAAK,EAAExE,YAAYwC,YAAY,EAAE,CAAC;wBACpDiC,gBAAgB;wBAChBC,oBAAoB;wBACpBC,cAAc;wBACdC,WAAW;oBACb;gBACF;YACF;SACD;IACH;IACAzB,QAAQY,IAAI,CAAC;QACXvE,GAAG;QACH;YACE6B,KAAK7B,GAAG;YACR8B,aAAahC,MAAMuF,kCAAkC;YACrDrD,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBAAEoD,QAAQ,GAAE/C,cAAAA,MAAMgD,IAAI,YAAVhD,cAAc;YAAG;QACtC;KACD;IACD,oEAAoE;IACpE,2DAA2D;IAC3DoB,QAAQY,IAAI,CAAC;QACXvE,GAAG;QACH;YACE6B,KAAK7B,GAAG;YACR8B,aAAahC,MAAM0F,+BAA+B;YAClDxD,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBAAEuD,SAAS;gBAAoBC,OAAO;gBAAGrE,IAAI;oBAAEc,YAAY;gBAAE;YAAE;QACxE;KACD;IACDwB,QAAQY,IAAI,CAAC;QACXvE,GAAG;QACH;YACE6B,KAAK7B,GAAG;YACR8B,aAAahC,MAAM6F,6BAA6B;YAChD3D,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO,CAAC;QACV;KACD;IACDyB,QAAQY,IAAI,CAAC;QACXvE,GAAG;QACH;YACE6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBACL0D,MAAM,CAAC,CAAC,EAAElC,WAAWmC,IAAI,EAAE;gBAC3B/B,UAAU,CAAC,EAAE,EAAEJ,WAAWoC,WAAW,EAAE;gBACvCzE,IAAI;oBAAE0E,WAAW;gBAAa;YAChC;QACF;KACD;IACD,OAAO,aACFtE,MACDkC,QAAQqC,GAAG,CAAC,CAAC,CAACC,QAAQ,GAAKA,UAC3BjF,eAECkF,OAAOC,WAAW,CAACxC;AAE1B;AAoBA;;;;;;;;CAQC,GACD,SAASyC,gBACP1C,UAA8B,EAC9B2C,UAAkC,EAClC5B,QAA+B;QAiBf4B,wBACAA;IAhBhB,wEAAwE;IACxE,gEAAgE;IAChE,4EAA4E;IAC5E,qEAAqE;IACrE,yEAAyE;IACzE,uEAAuE;IACvE,0DAA0D;IAC1D,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAEC,OAAO,EAAEC,OAAO,EAAE,GAAG3G,MAAM4G,yBAAyB,CAC5E;QACEC,gBAAgBjD,WAAWmC,IAAI;OAC3BpB,WAAW;QAAEmC,cAAcnC,SAASoB,IAAI;IAAC,IAAI,CAAC;QAClDS,MAAMD,WAAWC,IAAI;OACjBD,WAAWE,UAAU,KAAKjG,YAC1B,CAAC,IACD;QAAEiG,YAAYF,WAAWE,UAAU;IAAC;QACxCM,UAAU,GAAER,yBAAAA,WAAWQ,UAAU,YAArBR,yBAAyB;QACrCS,UAAU,GAAET,yBAAAA,WAAWS,UAAU,YAArBT,yBAAyB;;IAGzC,MAAMvC,WAAqB,EAAE;IAC7B,MAAM/B,QAAkB,CAAC;IACzB,IAAIyE,SAAS;QACXzE,KAAK,CAAC/B,GAAG,QAAQ,GAAG;YAClB6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBAAE0D,MAAMY;gBAAS1C,UAAU;YAAU;QAC9C;QACAA,SAASS,IAAI,CAACvE,GAAG;IACnB;IACA+B,KAAK,CAAC/B,GAAG,YAAY,GAAG;QACtB6B,KAAK7B,GAAG;QACR8B,aAAa;QACbE,UAAU;QACVC,UAAUjC,GAAG;QACbkC,OAAO;YACL0B,SAAS;YACTE,UAAUyC,aAAa,IAAI,CAAC,KAAK,EAAED,KAAK,IAAI,EAAEC,YAAY,GAAG,CAAC,KAAK,EAAED,MAAM;YAC3EjF,IAAI;gBAAE0F,OAAO;YAAiB;QAChC;IACF;IACAjD,SAASS,IAAI,CAACvE,GAAG;IACjB,IAAIyG,SAAS;QACX1E,KAAK,CAAC/B,GAAG,QAAQ,GAAG;YAClB6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBAAE0D,MAAMa;gBAAS3C,UAAU;YAAU;QAC9C;QACAA,SAASS,IAAI,CAACvE,GAAG;IACnB;IACA+B,KAAK,CAAC/B,GAAG,SAAS,GAAG;QACnB6B,KAAK7B,GAAG;QACR8B,aAAa;QACbE,UAAU;QACVC,UAAUjC,GAAG;QACbkC,OAAO;YACL8E,WAAW;YACX7F,SAAS;YACTE,IAAI;gBAAE4F,YAAY;gBAAUC,gBAAgB;YAAS;QACvD;QACAnF,OAAO+B;IACT;IACA,OAAO;QAAEqD,SAASnH,GAAG;QAAU+B;IAAM;AACvC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASqF,iCACd1D,UAA8B,EAC9B2D,UAAmB,EACnBhB,UAAmC,EACnC5B,QAA+B;QA0D1B4B;QA1CS3C;IAdd,MAAM,CAAC4D,SAAS/D,MAAM,GAAGlB,WAAW,SAAS;QAC3CuB,SAAS;QACTC,WAAW;QACX,mEAAmE;QACnE,wEAAwE;QACxE,gCAAgC;QAChCC,UAAUW,WACN,GAAGf,WAAWoC,WAAW,CAAC,GAAG,EAAErB,SAAS8C,IAAI,EAAE,GAC9C7D,WAAWoC,WAAW;IAC5B;IACA,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM0B,QAAQ9D,EAAAA,yBAAAA,WAAWW,UAAU,qBAArBX,uBAAuBY,MAAM,IACvC;QACEzC,KAAK7B,GAAG;QACR8B,aAAahC,MAAM2H,kCAAkC;QACrDzF,UAAU;QACVC,UAAUjC,GAAG;QACbkC,OAAO;YAAEwF,UAAU;QAAM;IAC3B,IACA;IACJ,MAAMC,OAAOH,QAAQ;QAACF;QAAStH,GAAG;KAAS,GAAG;QAACsH;KAAQ;IACvD,IAAI,CAACD,YAAY;QACf,MAAM,CAACO,SAASC,MAAM,GAAGxF,WAAW,SAAS;YAC3CuB,SAAS;YACTE,UAAUW,WACN,CAAC,qBAAqB,EAAEA,SAAS8C,IAAI,CAAC,KAAK,CAAC,GAC5C;YACJlG,IAAI;gBAAE0F,OAAO;YAAiB;QAChC;QACA,OAAO,aACFtF,MAAM;eAAIkG;YAAMC;SAAQ,EAAExG;YAC7B,CAACkG,QAAQ,EAAE/D;WACPiE,QAAQ;YAAE,CAACxH,GAAG,SAAS,EAAEwH;QAAM,IAAI,CAAC;YACxC,CAACI,QAAQ,EAAEC;;IAEf;IACA,MAAMC,OAAO,CAAC7H,QAAgBiC,QAAoC,CAAA;YAChEL,KAAK7B,GAAGC;YACR6B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC;QACF,CAAA;IACA;;;;;GAKC,GACD,MAAM6F,QACJ1B,cACCnC,CAAAA,QAAQmC,WAAWQ,UAAU,KAC5BR,WAAWC,IAAI,GAAG,KAClB,EAACD,yBAAAA,WAAWE,UAAU,YAArBF,yBAAyB,KAAK,CAAA,IAC7BD,gBAAgB1C,YAAY2C,YAAY5B,YACxC;IACN,OAAO,aACFhD,MACDsG,QACI;WAAIJ;QAAM3H,GAAG;QAAY+H,MAAMZ,OAAO;KAAC,GACvC;WAAIQ;QAAM3H,GAAG;KAAW,EAC5BoB;QAEF,CAACkG,QAAQ,EAAE/D;OACPiE,QAAQ;QAAE,CAACxH,GAAG,SAAS,EAAEwH;IAAM,IAAI,CAAC;QACxC,CAACxH,GAAG,WAAW,EAAE;YACf6B,KAAK7B,GAAG;YACR8B,aAAahC,MAAMkI,+BAA+B;YAClDhG,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBACLf,SAAS;eACLkF,aACA;gBAAE4B,SAAS5B,WAAW4B,OAAO;gBAAE3B,MAAMD,WAAWC,IAAI;YAAC,IACrD,CAAC;YAEPvE,OAAO;gBAAC/B,GAAG;aAAQ;QACrB;OACI+H,QAAQA,MAAMhG,KAAK,GAAG,CAAC;QAC3B,CAAC/B,GAAG,QAAQ,EAAE;YACZ6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBAAEf,SAAS;YAAI;YACtBY,OAAO;gBACL/B,GAAG;gBACHA,GAAG;gBACHA,GAAG;gBACHA,GAAG;aACJ;QACH;QACA,CAACA,GAAG,cAAc,EAAE8H,KAAK,cAAc;YACrClE,SAAS;YACTC,WAAW;YACXC,UAAU;QACZ;QACA,CAAC9D,GAAG,aAAa,EAAE8H,KAAK,aAAa;YACnClE,SAAS;YACTE,UAAU;YACVzC,IAAI;gBAAE0F,OAAO;YAAiB;QAChC;QACA,CAAC/G,GAAG,gBAAgB,EAAE8H,KAAK,gBAAgB;YACzClE,SAAS;YACTE,UAAU;QACZ;QACA,CAAC9D,GAAG,aAAa,EAAE;YACjB6B,KAAK7B,GAAG;YACR8B,aAAa;YACbE,UAAU;YACVC,UAAUjC,GAAG;YACbkC,OAAO;gBACL0D,MAAM;gBACN9B,UAAU;gBACVoE,MAAM;gBACN7G,IAAI;oBAAE0E,WAAW;gBAAa;YAChC;QACF;;AAEJ;AAEA,uEAAuE,GACvE,OAAO,SAASoC,6BAA6BC,OAU5C;QAWOA,qBACAA;IAXN,OAAOA,QAAQ7F,KAAK,GAChBkB,kCACE2E,QAAQ1E,UAAU,EAClB0E,QAAQ7F,KAAK,EACb6F,QAAQvF,MAAM,EACduF,QAAQhI,QAAQ,IAElBgH,iCACEgB,QAAQ1E,UAAU,EAClB0E,QAAQzE,OAAO,CAACW,MAAM,GAAG,IACzB8D,sBAAAA,QAAQ/B,UAAU,YAAlB+B,sBAAsB9H,YACtB8H,oBAAAA,QAAQ3D,QAAQ,YAAhB2D,oBAAoB9H;AAE5B;AAEA,eAAe6H,6BAA4B"}
|
|
@@ -85,6 +85,11 @@ export interface ComposedAuthorPage {
|
|
|
85
85
|
*/
|
|
86
86
|
export declare function composeAuthorTemplatePage(options: {
|
|
87
87
|
hostId: string;
|
|
88
|
+
/**
|
|
89
|
+
* The zone this site's dates read in (AGL-3237); UTC when a site has not
|
|
90
|
+
* named one. Resolved once by the caller that holds the org and the host.
|
|
91
|
+
*/
|
|
92
|
+
timeZone?: string;
|
|
88
93
|
host: any;
|
|
89
94
|
content: AuthorContent;
|
|
90
95
|
}): Promise<ComposedAuthorPage | null>;
|
|
@@ -103,6 +108,11 @@ export declare function composeAuthorTemplatePage(options: {
|
|
|
103
108
|
*/
|
|
104
109
|
export declare function composeAuthorFallbackPage(options: {
|
|
105
110
|
hostId: string;
|
|
111
|
+
/**
|
|
112
|
+
* The zone this site's dates read in (AGL-3237); UTC when a site has not
|
|
113
|
+
* named one. Resolved once by the caller that holds the org and the host.
|
|
114
|
+
*/
|
|
115
|
+
timeZone?: string;
|
|
106
116
|
host: any;
|
|
107
117
|
content: AuthorContent;
|
|
108
118
|
}): Promise<ComposedAuthorPage | null>;
|
|
@@ -194,7 +194,9 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
|
|
|
194
194
|
});
|
|
195
195
|
const { cardImages, tokens, collection } = authorComposeContext(content);
|
|
196
196
|
const card = collectSocialImageFacts(cardImages);
|
|
197
|
-
const nodes = await composeNodesWithChrome({
|
|
197
|
+
const nodes = await composeNodesWithChrome(_extends({}, options.timeZone ? {
|
|
198
|
+
timeZone: options.timeZone
|
|
199
|
+
} : {}, {
|
|
198
200
|
hostId,
|
|
199
201
|
layoutId,
|
|
200
202
|
socialImages: card.socialImages,
|
|
@@ -210,7 +212,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
|
|
|
210
212
|
tokens,
|
|
211
213
|
collection,
|
|
212
214
|
host: host
|
|
213
|
-
});
|
|
215
|
+
}));
|
|
214
216
|
return nodes ? _extends({
|
|
215
217
|
screen: null,
|
|
216
218
|
nodes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-author-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport buildAuthorPageNodes from './author-page-nodes'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from './compose-screen-nodes'\nimport type { AuthorContent } from './get-author-content'\nimport getScreen from './get-screen'\nimport { collectSocialImageFacts } from './social-image-facts'\n\n/**\n * The host field naming the screen every author page renders through\n * (AGL-2518).\n *\n * ## Why the HOST and not the author\n *\n * A collection names its own list and entry templates because a site\n * legitimately designs its changelog differently from its blog. Author pages\n * are not like that: they are one page shape repeated per person, and letting\n * each author record name its own template would mean a masthead where the\n * design changes as the reader clicks between colleagues — plus a template\n * picker on a form whose whole subject is a byline.\n *\n * ## Why a SCREEN and not a layout\n *\n * The opposite call from `builtInPageLayoutId` (AGL-2513), and for the reason\n * that comment gives in reverse. Search results have no design worth\n * authoring — the platform composes the body and the site only chooses the\n * chrome around it. An author page has: a portrait, a bio, a role, links, and\n * a listing whose cards a designer wants to match the blog's. That is a page,\n * so the author designs it as one, with `{{author.*}}` tokens and the same\n * Collection entries block every list template uses.\n *\n * Unset is the ordinary case rather than a misconfiguration, and it renders\n * the built-in body — bylines link to this address, so it must always answer.\n */\nexport const AUTHOR_PAGE_SCREEN_FIELD = 'authorScreenId' as const\n\n/**\n * The pseudo-collection an author page's entries block resolves against.\n *\n * The compose pipeline keys entry sources by collection slug, and this page\n * has no single collection — it mixes them. Its entries are handed over\n * already in hand (`ComposeCollectionContext.entries`), which is a path that\n * never touches Firestore, so the key only has to be a name that no real\n * collection can take: a collection slug is a URL segment, and this is not a\n * legal one.\n *\n * It never reaches a URL. Every entry on this page carries its OWN\n * `collectionSlug` out of the read that found it, and `collectionEntryTokens`\n * prefers that — which is the whole reason a cross-collection listing can\n * build correct `entry.url`s at all.\n */\nexport const AUTHOR_ENTRIES_SOURCE_SLUG = '__author__'\n\n/** What the author route renders. */\nexport interface ComposedAuthorPage {\n /** The template screen doc, when one is designated; null for the built-in. */\n screen: Record<string, any> | null\n nodes: Record<string, any>\n /**\n * The current pair of each picture the head's card may name (AGL-2850): the\n * author's share card, then their portrait. Read in the composition's\n * batch, and absent when it answered for neither.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/** The tokens and entry context both compose paths share. */\nfunction authorComposeContext(content: AuthorContent) {\n const address = content.author\n ? { author: content.author }\n : { authorName: content.slug }\n const pager = Aglyn.contentAuthorPaginationLinks({\n ...address,\n page: content.page,\n totalPages: content.totalPages,\n })\n return {\n /**\n * The pictures the head builds the card from, in its order: the share\n * card, then the portrait (AGL-2850). The author head reads the record\n * alone and never the site default, so the host's image is not one.\n */\n cardImages: [content.author?.seoImage, content.author?.image],\n tokens: {\n ...Aglyn.contentAuthorTokens(content.author, {\n entryCount: content.totalEntries,\n }),\n // The byline falls back to the raw segment for an unknown author, so a\n // heading bound to `{{author.name}}` prints something either way.\n 'author.name': content.name,\n // Named `pagination.*` rather than `author.page*`, so a designer who has\n // already built a pager on a collection list template rebuilds nothing\n // here — and so the two pagers cannot drift into two spellings of the\n // same four values. The edges are the empty string, which is what makes\n // an unconditional binding correct on page 1 of 1.\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n } as Record<string, string>,\n collection: {\n slug: AUTHOR_ENTRIES_SOURCE_SLUG,\n // The slug above is a key, not an address (AGL-2524) — so a block that\n // would build a URL out of it resolves nothing instead.\n routeless: true,\n // Already narrowed to this author AND to this page's window by the\n // loader, for the reason the category route states: a page count\n // computed over the whole set advertises pages that render empty.\n entries: content.entries,\n categories: content.categories,\n page: content.page,\n },\n }\n}\n\n/**\n * An author page composed through the screen the host designated (AGL-2518).\n *\n * The `composeCollectionTemplatePage` shape: the screen goes through the\n * NORMAL published pipeline — theme, shared layout, reusable components,\n * Collection entries blocks — with `{{author.*}}` and `{{pagination.*}}`\n * substituted and Author Profile blocks filled from the record.\n *\n * Returns null when no screen is designated, or when the designated one has\n * been deleted, so the caller falls through to the built-in body rather than\n * serving a 404 for a page a byline links to.\n */\nexport async function composeAuthorTemplatePage(options: {\n hostId: string\n host: any\n content: AuthorContent\n}): Promise<ComposedAuthorPage | null> {\n const { hostId, host, content } = options\n const screenId = String(host?.[AUTHOR_PAGE_SCREEN_FIELD] ?? '').trim()\n if (!screenId) return null\n try {\n // `allowTemplate`, exactly as a collection template is read: the screen is\n // deliberately not servable at an address of its own, and this is the\n // composition it exists for.\n const templateRes = await getScreen({\n hostId,\n screenId,\n allowTemplate: true,\n })\n if (!templateRes.screen) return null\n const { cardImages, tokens, collection } = authorComposeContext(content)\n const card = collectSocialImageFacts(cardImages)\n const nodes = await composeScreenNodes({\n hostId,\n screenId,\n screen: templateRes.screen,\n tokens,\n collection,\n socialImages: card.socialImages,\n // The designated screen's host variables, and its layout's, fill in\n // from this site, as the built-in author page's already do (AGL-2883).\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return null\n /*\n The screen document travels UNCHANGED, and deliberately carries no\n composed SEO.\n\n A collection template composes the entry's title and cover into\n `screen.seo` because `page.tsx` reads that on its way to the head. An\n author page does not take that path: its head branch has the author\n RECORD in hand and builds the title, description and portrait from it\n directly, before the screen branch is ever reached. Writing a second,\n unread copy here would be a value that looks authoritative, is dead, and\n goes stale the first time the head's rule changes — which is how a\n template's stored title came to name every post in a collection\n (AGL-1345).\n */\n return {\n screen: templateRes.screen as Record<string, any>,\n nodes: Aglyn.expandContentAuthorProfile(nodes, content.author),\n ...card.collected(),\n }\n } catch (error) {\n console.error('author template composition failed', error)\n return null\n }\n}\n\n/**\n * The built-in author page (AGL-2518) — the body this file's own nodes build,\n * grafted into the site's shared layout so it carries the same header, nav and\n * footer as the posts it links to.\n *\n * Which layout is the host's to choose (`resolveBuiltInPageLayoutId`), shared\n * with site search so the two platform-built pages of a site cannot end up in\n * different chrome.\n *\n * Fail-open to `null`, and the route then renders the body with no chrome at\n * all. A person's page must not be the page that 500s because a layout was\n * deleted.\n */\nexport async function composeAuthorFallbackPage(options: {\n hostId: string\n host: any\n content: AuthorContent\n}): Promise<ComposedAuthorPage | null> {\n const { hostId, host, content } = options\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const { cardImages, tokens, collection } = authorComposeContext(content)\n const card = collectSocialImageFacts(cardImages)\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n socialImages: card.socialImages,\n screenNodes: buildAuthorPageNodes({\n slug: content.slug,\n name: content.name,\n author: content.author,\n hasEntries: content.entries.length > 0,\n page: content.page,\n perPage: content.perPage,\n totalPages: content.totalPages,\n }),\n tokens,\n collection,\n host: host as Aglyn.HostTokenSource,\n })\n return nodes ? { screen: null, nodes, ...card.collected() } : null\n } catch (error) {\n console.error('author page composition failed', error)\n return null\n }\n}\n\nexport default composeAuthorTemplatePage\n"],"names":["Aglyn","buildAuthorPageNodes","resolveBuiltInPageLayoutId","composeScreenNodes","composeNodesWithChrome","getScreen","collectSocialImageFacts","AUTHOR_PAGE_SCREEN_FIELD","AUTHOR_ENTRIES_SOURCE_SLUG","authorComposeContext","content","address","author","authorName","slug","pager","contentAuthorPaginationLinks","page","totalPages","cardImages","seoImage","image","tokens","contentAuthorTokens","entryCount","totalEntries","name","String","prevUrl","nextUrl","collection","routeless","entries","categories","composeAuthorTemplatePage","options","hostId","host","screenId","trim","templateRes","allowTemplate","screen","card","nodes","socialImages","expandContentAuthorProfile","collected","error","console","composeAuthorFallbackPage","layoutId","screenNodes","hasEntries","length","perPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-author-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport buildAuthorPageNodes from './author-page-nodes'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from './compose-screen-nodes'\nimport type { AuthorContent } from './get-author-content'\nimport getScreen from './get-screen'\nimport { collectSocialImageFacts } from './social-image-facts'\n\n/**\n * The host field naming the screen every author page renders through\n * (AGL-2518).\n *\n * ## Why the HOST and not the author\n *\n * A collection names its own list and entry templates because a site\n * legitimately designs its changelog differently from its blog. Author pages\n * are not like that: they are one page shape repeated per person, and letting\n * each author record name its own template would mean a masthead where the\n * design changes as the reader clicks between colleagues — plus a template\n * picker on a form whose whole subject is a byline.\n *\n * ## Why a SCREEN and not a layout\n *\n * The opposite call from `builtInPageLayoutId` (AGL-2513), and for the reason\n * that comment gives in reverse. Search results have no design worth\n * authoring — the platform composes the body and the site only chooses the\n * chrome around it. An author page has: a portrait, a bio, a role, links, and\n * a listing whose cards a designer wants to match the blog's. That is a page,\n * so the author designs it as one, with `{{author.*}}` tokens and the same\n * Collection entries block every list template uses.\n *\n * Unset is the ordinary case rather than a misconfiguration, and it renders\n * the built-in body — bylines link to this address, so it must always answer.\n */\nexport const AUTHOR_PAGE_SCREEN_FIELD = 'authorScreenId' as const\n\n/**\n * The pseudo-collection an author page's entries block resolves against.\n *\n * The compose pipeline keys entry sources by collection slug, and this page\n * has no single collection — it mixes them. Its entries are handed over\n * already in hand (`ComposeCollectionContext.entries`), which is a path that\n * never touches Firestore, so the key only has to be a name that no real\n * collection can take: a collection slug is a URL segment, and this is not a\n * legal one.\n *\n * It never reaches a URL. Every entry on this page carries its OWN\n * `collectionSlug` out of the read that found it, and `collectionEntryTokens`\n * prefers that — which is the whole reason a cross-collection listing can\n * build correct `entry.url`s at all.\n */\nexport const AUTHOR_ENTRIES_SOURCE_SLUG = '__author__'\n\n/** What the author route renders. */\nexport interface ComposedAuthorPage {\n /** The template screen doc, when one is designated; null for the built-in. */\n screen: Record<string, any> | null\n nodes: Record<string, any>\n /**\n * The current pair of each picture the head's card may name (AGL-2850): the\n * author's share card, then their portrait. Read in the composition's\n * batch, and absent when it answered for neither.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/** The tokens and entry context both compose paths share. */\nfunction authorComposeContext(content: AuthorContent) {\n const address = content.author\n ? { author: content.author }\n : { authorName: content.slug }\n const pager = Aglyn.contentAuthorPaginationLinks({\n ...address,\n page: content.page,\n totalPages: content.totalPages,\n })\n return {\n /**\n * The pictures the head builds the card from, in its order: the share\n * card, then the portrait (AGL-2850). The author head reads the record\n * alone and never the site default, so the host's image is not one.\n */\n cardImages: [content.author?.seoImage, content.author?.image],\n tokens: {\n ...Aglyn.contentAuthorTokens(content.author, {\n entryCount: content.totalEntries,\n }),\n // The byline falls back to the raw segment for an unknown author, so a\n // heading bound to `{{author.name}}` prints something either way.\n 'author.name': content.name,\n // Named `pagination.*` rather than `author.page*`, so a designer who has\n // already built a pager on a collection list template rebuilds nothing\n // here — and so the two pagers cannot drift into two spellings of the\n // same four values. The edges are the empty string, which is what makes\n // an unconditional binding correct on page 1 of 1.\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n } as Record<string, string>,\n collection: {\n slug: AUTHOR_ENTRIES_SOURCE_SLUG,\n // The slug above is a key, not an address (AGL-2524) — so a block that\n // would build a URL out of it resolves nothing instead.\n routeless: true,\n // Already narrowed to this author AND to this page's window by the\n // loader, for the reason the category route states: a page count\n // computed over the whole set advertises pages that render empty.\n entries: content.entries,\n categories: content.categories,\n page: content.page,\n },\n }\n}\n\n/**\n * An author page composed through the screen the host designated (AGL-2518).\n *\n * The `composeCollectionTemplatePage` shape: the screen goes through the\n * NORMAL published pipeline — theme, shared layout, reusable components,\n * Collection entries blocks — with `{{author.*}}` and `{{pagination.*}}`\n * substituted and Author Profile blocks filled from the record.\n *\n * Returns null when no screen is designated, or when the designated one has\n * been deleted, so the caller falls through to the built-in body rather than\n * serving a 404 for a page a byline links to.\n */\nexport async function composeAuthorTemplatePage(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237); UTC when a site has not\n * named one. Resolved once by the caller that holds the org and the host.\n */\n timeZone?: string\n host: any\n content: AuthorContent\n}): Promise<ComposedAuthorPage | null> {\n const { hostId, host, content } = options\n const screenId = String(host?.[AUTHOR_PAGE_SCREEN_FIELD] ?? '').trim()\n if (!screenId) return null\n try {\n // `allowTemplate`, exactly as a collection template is read: the screen is\n // deliberately not servable at an address of its own, and this is the\n // composition it exists for.\n const templateRes = await getScreen({\n hostId,\n screenId,\n allowTemplate: true,\n })\n if (!templateRes.screen) return null\n const { cardImages, tokens, collection } = authorComposeContext(content)\n const card = collectSocialImageFacts(cardImages)\n const nodes = await composeScreenNodes({\n hostId,\n screenId,\n screen: templateRes.screen,\n tokens,\n collection,\n socialImages: card.socialImages,\n // The designated screen's host variables, and its layout's, fill in\n // from this site, as the built-in author page's already do (AGL-2883).\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return null\n /*\n The screen document travels UNCHANGED, and deliberately carries no\n composed SEO.\n\n A collection template composes the entry's title and cover into\n `screen.seo` because `page.tsx` reads that on its way to the head. An\n author page does not take that path: its head branch has the author\n RECORD in hand and builds the title, description and portrait from it\n directly, before the screen branch is ever reached. Writing a second,\n unread copy here would be a value that looks authoritative, is dead, and\n goes stale the first time the head's rule changes — which is how a\n template's stored title came to name every post in a collection\n (AGL-1345).\n */\n return {\n screen: templateRes.screen as Record<string, any>,\n nodes: Aglyn.expandContentAuthorProfile(nodes, content.author),\n ...card.collected(),\n }\n } catch (error) {\n console.error('author template composition failed', error)\n return null\n }\n}\n\n/**\n * The built-in author page (AGL-2518) — the body this file's own nodes build,\n * grafted into the site's shared layout so it carries the same header, nav and\n * footer as the posts it links to.\n *\n * Which layout is the host's to choose (`resolveBuiltInPageLayoutId`), shared\n * with site search so the two platform-built pages of a site cannot end up in\n * different chrome.\n *\n * Fail-open to `null`, and the route then renders the body with no chrome at\n * all. A person's page must not be the page that 500s because a layout was\n * deleted.\n */\nexport async function composeAuthorFallbackPage(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237); UTC when a site has not\n * named one. Resolved once by the caller that holds the org and the host.\n */\n timeZone?: string\n host: any\n content: AuthorContent\n}): Promise<ComposedAuthorPage | null> {\n const { hostId, host, content } = options\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const { cardImages, tokens, collection } = authorComposeContext(content)\n const card = collectSocialImageFacts(cardImages)\n const nodes = await composeNodesWithChrome({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n hostId,\n layoutId,\n socialImages: card.socialImages,\n screenNodes: buildAuthorPageNodes({\n slug: content.slug,\n name: content.name,\n author: content.author,\n hasEntries: content.entries.length > 0,\n page: content.page,\n perPage: content.perPage,\n totalPages: content.totalPages,\n }),\n tokens,\n collection,\n host: host as Aglyn.HostTokenSource,\n })\n return nodes ? { screen: null, nodes, ...card.collected() } : null\n } catch (error) {\n console.error('author page composition failed', error)\n return null\n }\n}\n\nexport default composeAuthorTemplatePage\n"],"names":["Aglyn","buildAuthorPageNodes","resolveBuiltInPageLayoutId","composeScreenNodes","composeNodesWithChrome","getScreen","collectSocialImageFacts","AUTHOR_PAGE_SCREEN_FIELD","AUTHOR_ENTRIES_SOURCE_SLUG","authorComposeContext","content","address","author","authorName","slug","pager","contentAuthorPaginationLinks","page","totalPages","cardImages","seoImage","image","tokens","contentAuthorTokens","entryCount","totalEntries","name","String","prevUrl","nextUrl","collection","routeless","entries","categories","composeAuthorTemplatePage","options","hostId","host","screenId","trim","templateRes","allowTemplate","screen","card","nodes","socialImages","expandContentAuthorProfile","collected","error","console","composeAuthorFallbackPage","layoutId","timeZone","screenNodes","hasEntries","length","perPage"],"mappings":"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"}
|
|
@@ -71,6 +71,11 @@ export interface ComposedCollectionPage {
|
|
|
71
71
|
*/
|
|
72
72
|
export declare function composeCollectionTemplatePage(options: {
|
|
73
73
|
hostId: string;
|
|
74
|
+
/**
|
|
75
|
+
* The zone this site's dates read in (AGL-3237); UTC when a site has not
|
|
76
|
+
* named one. Resolved once by the caller that holds the org and the host.
|
|
77
|
+
*/
|
|
78
|
+
timeZone?: string;
|
|
74
79
|
content: CollectionContent;
|
|
75
80
|
/**
|
|
76
81
|
* The site, whose default card the head falls back to after the entry's
|
|
@@ -88,6 +93,11 @@ export declare function composeCollectionTemplatePage(options: {
|
|
|
88
93
|
*/
|
|
89
94
|
export declare function composeCollectionFallbackPage(options: {
|
|
90
95
|
hostId: string;
|
|
96
|
+
/**
|
|
97
|
+
* The zone this site's dates read in (AGL-3237); UTC when a site has not
|
|
98
|
+
* named one. Resolved once by the caller that holds the org and the host.
|
|
99
|
+
*/
|
|
100
|
+
timeZone?: string;
|
|
91
101
|
host: Aglyn.AglynHost;
|
|
92
102
|
content: CollectionContent;
|
|
93
103
|
}): Promise<Omit<ComposedCollectionPage, 'screen'> | null>;
|
|
@@ -204,7 +204,9 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
|
|
|
204
204
|
hostId,
|
|
205
205
|
host
|
|
206
206
|
});
|
|
207
|
-
const screenNodes = buildCollectionFallbackNodes({
|
|
207
|
+
const screenNodes = buildCollectionFallbackNodes(_extends({}, options.timeZone ? {
|
|
208
|
+
timeZone: options.timeZone
|
|
209
|
+
} : {}, {
|
|
208
210
|
collection,
|
|
209
211
|
entries: content.entries,
|
|
210
212
|
entry: content.entry,
|
|
@@ -214,14 +216,16 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
|
|
|
214
216
|
// org-scoped reference has to name the site asking or a site-restricted
|
|
215
217
|
// asset will not serve.
|
|
216
218
|
hostId
|
|
217
|
-
});
|
|
219
|
+
}));
|
|
218
220
|
// The head's card on a page with no template: the entry's cover, then the
|
|
219
221
|
// site default (AGL-2850).
|
|
220
222
|
const card = collectSocialImageFacts([
|
|
221
223
|
(_content_entry = content.entry) == null ? void 0 : _content_entry.coverImage,
|
|
222
224
|
host == null ? void 0 : (_host_seo = host.seo) == null ? void 0 : _host_seo.image
|
|
223
225
|
]);
|
|
224
|
-
const nodes = await composeNodesWithChrome({
|
|
226
|
+
const nodes = await composeNodesWithChrome(_extends({}, options.timeZone ? {
|
|
227
|
+
timeZone: options.timeZone
|
|
228
|
+
} : {}, {
|
|
225
229
|
hostId,
|
|
226
230
|
layoutId,
|
|
227
231
|
screenNodes,
|
|
@@ -250,7 +254,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
|
|
|
250
254
|
} : {}, ((_content_pagination1 = content.pagination) == null ? void 0 : _content_pagination1.windowStart) ? {
|
|
251
255
|
windowStart: content.pagination.windowStart
|
|
252
256
|
} : {})
|
|
253
|
-
});
|
|
257
|
+
}));
|
|
254
258
|
return nodes ? _extends({
|
|
255
259
|
nodes
|
|
256
260
|
}, card.collected()) : null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-collection-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport buildCollectionFallbackNodes from './collection-fallback-nodes'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from './compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport type { CollectionContent } from './get-collection-content'\nimport getScreen from './get-screen'\nimport { collectSocialImageFacts } from './social-image-facts'\n\ntype CollectionDoc = NonNullable<CollectionContent['collection']>\n\n/**\n * Which template screen a collection route renders through (AGL-551):\n * `/{collection}` uses `listScreenId`, `/{collection}/{entry}` uses\n * `entryScreenId` — falling back to the legacy AGL-105 `templateScreenId`\n * so existing blogs keep rendering. `undefined` means no template is set\n * and the designed built-in fallback applies.\n */\nexport function resolveCollectionTemplateScreenId(\n collection: Pick<\n CollectionDoc,\n 'listScreenId' | 'entryScreenId' | 'templateScreenId'\n >,\n kind: 'list' | 'entry',\n): string | undefined {\n if (kind === 'list') return collection.listScreenId || undefined\n return collection.entryScreenId || collection.templateScreenId || undefined\n}\n\n/**\n * Page-level `{{collection.*}}` tokens for template screens (AGL-551), plus\n * the routed category (AGL-1321) so a list template can name what it is\n * showing — \"Guides\" rather than a heading that says \"Blog\" on every filtered\n * URL. Both category tokens resolve to the empty string on the unfiltered\n * listing, which is what makes them safe to bind unconditionally.\n *\n * `{{pagination.*}}` follows the same design (AGL-1386). One static list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, so a hand-built pager on it renders identically on all\n * of them: \"Older →\" on a category that has one page, pointing at a URL that\n * dropped the filter. The URLs come from `collectionPaginationLinks`, which\n * builds them through the shared listing-URL builder (so they carry the\n * category) and resolves the EDGES TO THE EMPTY STRING — no previous page,\n * no `prevUrl`. There is no runtime conditional to hide a link with (the\n * `condition` field on nodes is editor-side field visibility, not a render\n * gate), so the empty string is what makes an unconditional binding correct\n * on every route: a link whose href does not resolve renders as an inert\n * placeholder of the same element (AGL-1268/1357).\n *\n * This SURFACES what the platform already computes — the built-in fallback\n * pager reads the same function, so the two cannot drift.\n */\nexport function collectionTokens(\n collection: Pick<CollectionDoc, 'displayName' | 'slug'>,\n category?: CollectionContent['category'],\n pagination?: CollectionContent['pagination'],\n): Record<string, string> {\n // An unpaginated listing is page 1 of 1 — both URLs empty, which reads as\n // the honest \"nowhere to page to\" rather than a broken link.\n const pager = Aglyn.collectionPaginationLinks({\n collectionSlug: collection.slug,\n ...(category?.slug ? { categorySlug: category.slug } : {}),\n page: pagination?.page,\n ...(pagination?.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n // Only a LISTING has cursors, so passing them is also what tells the\n // builder this is a listing and not an inert entry route (AGL-3219).\n ...(pagination\n ? {\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n }\n : {}),\n })\n return {\n 'collection.name': collection.displayName,\n 'collection.slug': collection.slug,\n 'collection.category': category?.name ?? '',\n 'collection.categorySlug': category?.slug ?? '',\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n }\n}\n\nexport interface ComposedCollectionPage {\n /** Template screen doc with the entry/collection SEO merged in. */\n screen: Record<string, any>\n nodes: Record<string, any>\n /**\n * The current pair of each asset the head's card may name (AGL-2850): the\n * entry's cover, the template's image, the site default. Read in the\n * composition's batch, and absent when it answered for none of them.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/**\n * Renders a collection route through its designated template screen\n * (AGL-551), the same mechanism as commerce PDP/collection templates: the\n * screen composes through the NORMAL published pipeline — theme, shared\n * layout, reusable components — with `{{entry.*}}`/`{{collection.*}}`\n * tokens substituted and Collection entries blocks expanded. Returns null\n * when no template is designated (or it fails to compose) so the caller\n * falls through to the designed built-in fallback.\n */\nexport async function composeCollectionTemplatePage(options: {\n hostId: string\n content: CollectionContent\n /**\n * The site, whose default card the head falls back to after the entry's\n * cover and the template's image (AGL-2850). Its document is read in this\n * page's batch with theirs.\n */\n host?: Aglyn.AglynHost | null\n}): Promise<ComposedCollectionPage | null> {\n const { hostId, content } = options\n const collection = content.collection\n if (!collection) return null\n const kind = content.entry ? 'entry' : 'list'\n const screenId = resolveCollectionTemplateScreenId(collection, kind)\n if (!screenId) return null\n\n // The one read that WANTS a template document (AGL-1400): this is the\n // composition it exists for, with `{{entry.*}}` substituted against the\n // routed entry. Every path-resolving caller leaves the flag off and gets the\n // 404 a template deserves at an address of its own.\n const templateRes = await getScreen({ hostId, screenId, allowTemplate: true })\n if (!templateRes.screen) return null\n\n const entry = content.entry\n const tokens = entry\n ? {\n ...collectionTokens(collection),\n // Category names resolve against the collection's taxonomy\n // (AGL-582): `categoryId` lookup first, legacy string fallback.\n ...Aglyn.collectionEntryTokens(\n entry,\n collection.slug,\n collection.categories,\n ),\n }\n : collectionTokens(collection, content.category, content.pagination)\n // The head's card on this page, in its order: the entry's cover, the\n // template's own image, then the site default (AGL-2850).\n const card = collectSocialImageFacts([\n entry?.coverImage,\n (templateRes.screen as any).seo?.image,\n options.host?.seo?.image,\n ])\n const nodes = await composeScreenNodes({\n hostId,\n screenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The site the template renders for, so its host variables — and its\n // layout's — fill in as they do on every other page (AGL-2883).\n host: options.host,\n tokens,\n // List pages hand their already-fetched entries to the Collection\n // entries block; entry pages carry the routed entry (AGL-582, Related\n // posts) and let blocks fetch entry lists on demand (e.g. a \"More\n // posts\" section on the article template). The category taxonomy\n // rides along so `{{entry.category}}` resolves inside the blocks.\n collection: entry\n ? { slug: collection.slug, entry, categories: collection.categories }\n : {\n slug: collection.slug,\n // Already filtered to the routed category (AGL-1321) — the block\n // repeats what the ROUTE resolved, so a designer-pinned\n // `filterCategory` on the block narrows it further rather than\n // fighting it.\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category ? { categorySlug: content.category.slug } : {}),\n // The read's own bound (AGL-1516), which has to travel WITH the\n // filtered entries above: once `content.entries` has been narrowed\n // to a category, nothing downstream can tell a complete read from a\n // truncated one by counting it.\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n // Where those entries start (AGL-3213): a listing page past the\n // cached read's bound holds its own window, not the collection\n // from the top, and the block has to window from the same origin.\n ...(content.pagination?.windowStart\n ? { windowStart: content.pagination.windowStart }\n : {}),\n },\n })\n if (!nodes) return null\n\n const screenSeo = (templateRes.screen as any).seo ?? {}\n const seo = entry\n ? // Entry metadata drives the head (AGL-117 merge; AGL-582 overrides).\n {\n ...screenSeo,\n title: entry.seoTitle || entry.title,\n description: entry.seoDescription || entry.excerpt || undefined,\n // The image and its companions move as ONE group (AGL-2417). The\n // spread above carries the SCREEN's `imageWidth`/`imageHeight`/\n // `imageAlt`, so an entry that supplies its own cover was describing\n // it with the screen default's size and — once alts existed — with\n // the screen default's DESCRIPTION: a sentence about a picture this\n // card does not show, delivered to the reader least able to check.\n // When the entry wins, its own companions win with it.\n ...(entry.coverImage\n ? {\n image: entry.coverImage,\n imageWidth: undefined,\n imageHeight: undefined,\n imageAlt: entry.coverImageAlt || undefined,\n }\n : { image: screenSeo.image || undefined }),\n }\n : // A LIST passes its screen's own SEO through UNTOUCHED (AGL-1345).\n //\n // This used to default `title` to `collection.displayName`, which reads\n // like a harmless fallback and is not: it made an authored title\n // indistinguishable from a generated one by the time the head was built.\n // The title rule (AGL-1341) turns on exactly that distinction — an\n // authored title renders VERBATIM, a name joins the site title — so a\n // consumer reading this could only choose between dropping the site\n // title off every untitled list (\"Changelog\") or ignoring the author's\n // title on every titled one (\"Changelog – Acme\" over the sentence they\n // wrote). The collection name is still the fallback; it just belongs to\n // the title resolver, as the page's `name`, alongside every other\n // surface's fallback rather than baked into stored SEO here.\n screenSeo\n return {\n screen: { ...(templateRes.screen as any), seo },\n nodes,\n ...card.collected(),\n }\n}\n\n/**\n * The designed built-in rendering (AGL-551): when a collection has no\n * template screen, its routes still compose through the site's theme and\n * the host's default shared layout (the home screen's layout) instead of\n * the old unthemed article. Fail-open — any error returns null and the\n * caller keeps the legacy plain rendering.\n */\nexport async function composeCollectionFallbackPage(options: {\n hostId: string\n host: Aglyn.AglynHost\n content: CollectionContent\n}): Promise<Omit<ComposedCollectionPage, 'screen'> | null> {\n const { hostId, host, content } = options\n const collection = content.collection\n if (!collection) return null\n try {\n // The layout for a page the platform composed rather than the author\n // (AGL-2513). Still the home screen's layout by default — the rule this\n // branch has always followed — but the host can now name a different one,\n // and site search reads the same setting so the two built-in pages of a\n // site cannot end up in different chrome.\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const screenNodes = buildCollectionFallbackNodes({\n collection,\n entries: content.entries,\n entry: content.entry,\n pagination: content.pagination,\n category: content.category,\n // The cover resolves through `resolveMediaSrc` (AGL-1407), and an\n // org-scoped reference has to name the site asking or a site-restricted\n // asset will not serve.\n hostId,\n })\n // The head's card on a page with no template: the entry's cover, then the\n // site default (AGL-2850).\n const card = collectSocialImageFacts([\n content.entry?.coverImage,\n host?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes,\n socialImages: card.socialImages,\n // The layout's host variables fill in from this site (AGL-2883).\n host,\n // Entry routes resolve with an EMPTY entries list (the loader only\n // fetched the one entry), so hand the routed entry over and let the\n // Related posts block fetch the list on demand (AGL-582); list\n // routes keep their already-fetched entries. Categories ride along\n // for `categoryId` → name resolution.\n collection: content.entry\n ? {\n slug: collection.slug,\n entry: content.entry,\n categories: collection.categories,\n }\n : {\n slug: collection.slug,\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category\n ? { categorySlug: content.category.slug }\n : {}),\n // Same fact, same reason as the template path above (AGL-1516).\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n // Same window, same reason as the template path above (AGL-3213).\n ...(content.pagination?.windowStart\n ? { windowStart: content.pagination.windowStart }\n : {}),\n },\n })\n return nodes ? { nodes, ...card.collected() } : null\n } catch (error) {\n console.error(error)\n return null\n }\n}\n\nexport default composeCollectionTemplatePage\n"],"names":["Aglyn","buildCollectionFallbackNodes","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","getScreen","collectSocialImageFacts","resolveCollectionTemplateScreenId","collection","kind","listScreenId","undefined","entryScreenId","templateScreenId","collectionTokens","category","pagination","pager","collectionPaginationLinks","collectionSlug","slug","categorySlug","page","totalPages","nextCursor","prevCursor","displayName","name","String","prevUrl","nextUrl","composeCollectionTemplatePage","options","content","hostId","entry","screenId","templateRes","allowTemplate","screen","tokens","collectionEntryTokens","categories","card","coverImage","seo","image","host","nodes","socialImages","entries","entriesReachedBound","windowStart","screenSeo","title","seoTitle","description","seoDescription","excerpt","imageWidth","imageHeight","imageAlt","coverImageAlt","collected","composeCollectionFallbackPage","layoutId","screenNodes","error","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-collection-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport buildCollectionFallbackNodes from './collection-fallback-nodes'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from './compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport type { CollectionContent } from './get-collection-content'\nimport getScreen from './get-screen'\nimport { collectSocialImageFacts } from './social-image-facts'\n\ntype CollectionDoc = NonNullable<CollectionContent['collection']>\n\n/**\n * Which template screen a collection route renders through (AGL-551):\n * `/{collection}` uses `listScreenId`, `/{collection}/{entry}` uses\n * `entryScreenId` — falling back to the legacy AGL-105 `templateScreenId`\n * so existing blogs keep rendering. `undefined` means no template is set\n * and the designed built-in fallback applies.\n */\nexport function resolveCollectionTemplateScreenId(\n collection: Pick<\n CollectionDoc,\n 'listScreenId' | 'entryScreenId' | 'templateScreenId'\n >,\n kind: 'list' | 'entry',\n): string | undefined {\n if (kind === 'list') return collection.listScreenId || undefined\n return collection.entryScreenId || collection.templateScreenId || undefined\n}\n\n/**\n * Page-level `{{collection.*}}` tokens for template screens (AGL-551), plus\n * the routed category (AGL-1321) so a list template can name what it is\n * showing — \"Guides\" rather than a heading that says \"Blog\" on every filtered\n * URL. Both category tokens resolve to the empty string on the unfiltered\n * listing, which is what makes them safe to bind unconditionally.\n *\n * `{{pagination.*}}` follows the same design (AGL-1386). One static list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, so a hand-built pager on it renders identically on all\n * of them: \"Older →\" on a category that has one page, pointing at a URL that\n * dropped the filter. The URLs come from `collectionPaginationLinks`, which\n * builds them through the shared listing-URL builder (so they carry the\n * category) and resolves the EDGES TO THE EMPTY STRING — no previous page,\n * no `prevUrl`. There is no runtime conditional to hide a link with (the\n * `condition` field on nodes is editor-side field visibility, not a render\n * gate), so the empty string is what makes an unconditional binding correct\n * on every route: a link whose href does not resolve renders as an inert\n * placeholder of the same element (AGL-1268/1357).\n *\n * This SURFACES what the platform already computes — the built-in fallback\n * pager reads the same function, so the two cannot drift.\n */\nexport function collectionTokens(\n collection: Pick<CollectionDoc, 'displayName' | 'slug'>,\n category?: CollectionContent['category'],\n pagination?: CollectionContent['pagination'],\n): Record<string, string> {\n // An unpaginated listing is page 1 of 1 — both URLs empty, which reads as\n // the honest \"nowhere to page to\" rather than a broken link.\n const pager = Aglyn.collectionPaginationLinks({\n collectionSlug: collection.slug,\n ...(category?.slug ? { categorySlug: category.slug } : {}),\n page: pagination?.page,\n ...(pagination?.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n // Only a LISTING has cursors, so passing them is also what tells the\n // builder this is a listing and not an inert entry route (AGL-3219).\n ...(pagination\n ? {\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n }\n : {}),\n })\n return {\n 'collection.name': collection.displayName,\n 'collection.slug': collection.slug,\n 'collection.category': category?.name ?? '',\n 'collection.categorySlug': category?.slug ?? '',\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n }\n}\n\nexport interface ComposedCollectionPage {\n /** Template screen doc with the entry/collection SEO merged in. */\n screen: Record<string, any>\n nodes: Record<string, any>\n /**\n * The current pair of each asset the head's card may name (AGL-2850): the\n * entry's cover, the template's image, the site default. Read in the\n * composition's batch, and absent when it answered for none of them.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/**\n * Renders a collection route through its designated template screen\n * (AGL-551), the same mechanism as commerce PDP/collection templates: the\n * screen composes through the NORMAL published pipeline — theme, shared\n * layout, reusable components — with `{{entry.*}}`/`{{collection.*}}`\n * tokens substituted and Collection entries blocks expanded. Returns null\n * when no template is designated (or it fails to compose) so the caller\n * falls through to the designed built-in fallback.\n */\nexport async function composeCollectionTemplatePage(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237); UTC when a site has not\n * named one. Resolved once by the caller that holds the org and the host.\n */\n timeZone?: string\n content: CollectionContent\n /**\n * The site, whose default card the head falls back to after the entry's\n * cover and the template's image (AGL-2850). Its document is read in this\n * page's batch with theirs.\n */\n host?: Aglyn.AglynHost | null\n}): Promise<ComposedCollectionPage | null> {\n const { hostId, content } = options\n const collection = content.collection\n if (!collection) return null\n const kind = content.entry ? 'entry' : 'list'\n const screenId = resolveCollectionTemplateScreenId(collection, kind)\n if (!screenId) return null\n\n // The one read that WANTS a template document (AGL-1400): this is the\n // composition it exists for, with `{{entry.*}}` substituted against the\n // routed entry. Every path-resolving caller leaves the flag off and gets the\n // 404 a template deserves at an address of its own.\n const templateRes = await getScreen({ hostId, screenId, allowTemplate: true })\n if (!templateRes.screen) return null\n\n const entry = content.entry\n const tokens = entry\n ? {\n ...collectionTokens(collection),\n // Category names resolve against the collection's taxonomy\n // (AGL-582): `categoryId` lookup first, legacy string fallback.\n ...Aglyn.collectionEntryTokens(\n entry,\n collection.slug,\n collection.categories,\n ),\n }\n : collectionTokens(collection, content.category, content.pagination)\n // The head's card on this page, in its order: the entry's cover, the\n // template's own image, then the site default (AGL-2850).\n const card = collectSocialImageFacts([\n entry?.coverImage,\n (templateRes.screen as any).seo?.image,\n options.host?.seo?.image,\n ])\n const nodes = await composeScreenNodes({\n hostId,\n screenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The site the template renders for, so its host variables — and its\n // layout's — fill in as they do on every other page (AGL-2883).\n host: options.host,\n tokens,\n // List pages hand their already-fetched entries to the Collection\n // entries block; entry pages carry the routed entry (AGL-582, Related\n // posts) and let blocks fetch entry lists on demand (e.g. a \"More\n // posts\" section on the article template). The category taxonomy\n // rides along so `{{entry.category}}` resolves inside the blocks.\n collection: entry\n ? { slug: collection.slug, entry, categories: collection.categories }\n : {\n slug: collection.slug,\n // Already filtered to the routed category (AGL-1321) — the block\n // repeats what the ROUTE resolved, so a designer-pinned\n // `filterCategory` on the block narrows it further rather than\n // fighting it.\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category ? { categorySlug: content.category.slug } : {}),\n // The read's own bound (AGL-1516), which has to travel WITH the\n // filtered entries above: once `content.entries` has been narrowed\n // to a category, nothing downstream can tell a complete read from a\n // truncated one by counting it.\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n // Where those entries start (AGL-3213): a listing page past the\n // cached read's bound holds its own window, not the collection\n // from the top, and the block has to window from the same origin.\n ...(content.pagination?.windowStart\n ? { windowStart: content.pagination.windowStart }\n : {}),\n },\n })\n if (!nodes) return null\n\n const screenSeo = (templateRes.screen as any).seo ?? {}\n const seo = entry\n ? // Entry metadata drives the head (AGL-117 merge; AGL-582 overrides).\n {\n ...screenSeo,\n title: entry.seoTitle || entry.title,\n description: entry.seoDescription || entry.excerpt || undefined,\n // The image and its companions move as ONE group (AGL-2417). The\n // spread above carries the SCREEN's `imageWidth`/`imageHeight`/\n // `imageAlt`, so an entry that supplies its own cover was describing\n // it with the screen default's size and — once alts existed — with\n // the screen default's DESCRIPTION: a sentence about a picture this\n // card does not show, delivered to the reader least able to check.\n // When the entry wins, its own companions win with it.\n ...(entry.coverImage\n ? {\n image: entry.coverImage,\n imageWidth: undefined,\n imageHeight: undefined,\n imageAlt: entry.coverImageAlt || undefined,\n }\n : { image: screenSeo.image || undefined }),\n }\n : // A LIST passes its screen's own SEO through UNTOUCHED (AGL-1345).\n //\n // This used to default `title` to `collection.displayName`, which reads\n // like a harmless fallback and is not: it made an authored title\n // indistinguishable from a generated one by the time the head was built.\n // The title rule (AGL-1341) turns on exactly that distinction — an\n // authored title renders VERBATIM, a name joins the site title — so a\n // consumer reading this could only choose between dropping the site\n // title off every untitled list (\"Changelog\") or ignoring the author's\n // title on every titled one (\"Changelog – Acme\" over the sentence they\n // wrote). The collection name is still the fallback; it just belongs to\n // the title resolver, as the page's `name`, alongside every other\n // surface's fallback rather than baked into stored SEO here.\n screenSeo\n return {\n screen: { ...(templateRes.screen as any), seo },\n nodes,\n ...card.collected(),\n }\n}\n\n/**\n * The designed built-in rendering (AGL-551): when a collection has no\n * template screen, its routes still compose through the site's theme and\n * the host's default shared layout (the home screen's layout) instead of\n * the old unthemed article. Fail-open — any error returns null and the\n * caller keeps the legacy plain rendering.\n */\nexport async function composeCollectionFallbackPage(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237); UTC when a site has not\n * named one. Resolved once by the caller that holds the org and the host.\n */\n timeZone?: string\n host: Aglyn.AglynHost\n content: CollectionContent\n}): Promise<Omit<ComposedCollectionPage, 'screen'> | null> {\n const { hostId, host, content } = options\n const collection = content.collection\n if (!collection) return null\n try {\n // The layout for a page the platform composed rather than the author\n // (AGL-2513). Still the home screen's layout by default — the rule this\n // branch has always followed — but the host can now name a different one,\n // and site search reads the same setting so the two built-in pages of a\n // site cannot end up in different chrome.\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const screenNodes = buildCollectionFallbackNodes({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n collection,\n entries: content.entries,\n entry: content.entry,\n pagination: content.pagination,\n category: content.category,\n // The cover resolves through `resolveMediaSrc` (AGL-1407), and an\n // org-scoped reference has to name the site asking or a site-restricted\n // asset will not serve.\n hostId,\n })\n // The head's card on a page with no template: the entry's cover, then the\n // site default (AGL-2850).\n const card = collectSocialImageFacts([\n content.entry?.coverImage,\n host?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n hostId,\n layoutId,\n screenNodes,\n socialImages: card.socialImages,\n // The layout's host variables fill in from this site (AGL-2883).\n host,\n // Entry routes resolve with an EMPTY entries list (the loader only\n // fetched the one entry), so hand the routed entry over and let the\n // Related posts block fetch the list on demand (AGL-582); list\n // routes keep their already-fetched entries. Categories ride along\n // for `categoryId` → name resolution.\n collection: content.entry\n ? {\n slug: collection.slug,\n entry: content.entry,\n categories: collection.categories,\n }\n : {\n slug: collection.slug,\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category\n ? { categorySlug: content.category.slug }\n : {}),\n // Same fact, same reason as the template path above (AGL-1516).\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n // Same window, same reason as the template path above (AGL-3213).\n ...(content.pagination?.windowStart\n ? { windowStart: content.pagination.windowStart }\n : {}),\n },\n })\n return nodes ? { nodes, ...card.collected() } : null\n } catch (error) {\n console.error(error)\n return null\n }\n}\n\nexport default composeCollectionTemplatePage\n"],"names":["Aglyn","buildCollectionFallbackNodes","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","getScreen","collectSocialImageFacts","resolveCollectionTemplateScreenId","collection","kind","listScreenId","undefined","entryScreenId","templateScreenId","collectionTokens","category","pagination","pager","collectionPaginationLinks","collectionSlug","slug","categorySlug","page","totalPages","nextCursor","prevCursor","displayName","name","String","prevUrl","nextUrl","composeCollectionTemplatePage","options","content","hostId","entry","screenId","templateRes","allowTemplate","screen","tokens","collectionEntryTokens","categories","card","coverImage","seo","image","host","nodes","socialImages","entries","entriesReachedBound","windowStart","screenSeo","title","seoTitle","description","seoDescription","excerpt","imageWidth","imageHeight","imageAlt","coverImageAlt","collected","composeCollectionFallbackPage","layoutId","screenNodes","timeZone","error","console"],"mappings":"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"}
|
|
@@ -90,6 +90,16 @@ export interface ComposeCollectionContext {
|
|
|
90
90
|
*/
|
|
91
91
|
export declare function composeNodesWithChrome(options: {
|
|
92
92
|
hostId: string;
|
|
93
|
+
/**
|
|
94
|
+
* The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of
|
|
95
|
+
* the org and the host, resolved ONCE by the caller that holds both.
|
|
96
|
+
*
|
|
97
|
+
* A string rather than the two documents, because the value has to be
|
|
98
|
+
* identical on the server render and the client re-render, and the surest
|
|
99
|
+
* way to guarantee that is for only one place to decide it. Absent is UTC,
|
|
100
|
+
* which is what every site rendered before this existed.
|
|
101
|
+
*/
|
|
102
|
+
timeZone?: string;
|
|
93
103
|
/**
|
|
94
104
|
* The layout binding, or a PROMISE of it.
|
|
95
105
|
*
|
|
@@ -159,6 +169,16 @@ export declare function composeScreenNodes(options: {
|
|
|
159
169
|
versionId?: string;
|
|
160
170
|
/** The host document, for `host.*` tokens (AGL-1022). */
|
|
161
171
|
host?: Aglyn.HostTokenSource | null;
|
|
172
|
+
/**
|
|
173
|
+
* The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of
|
|
174
|
+
* the org and the host, resolved ONCE by the caller that holds both.
|
|
175
|
+
*
|
|
176
|
+
* A string rather than the two documents, because the value has to be
|
|
177
|
+
* identical on the server render and the client re-render, and the surest
|
|
178
|
+
* way to guarantee that is for only one place to decide it. Absent is UTC,
|
|
179
|
+
* which is what every site rendered before this existed.
|
|
180
|
+
*/
|
|
181
|
+
timeZone?: string;
|
|
162
182
|
/** The social card the head shares this page as (AGL-2850). */
|
|
163
183
|
socialImages?: ComposeSocialImages;
|
|
164
184
|
}): Promise<Record<string, any> | null>;
|
|
@@ -131,7 +131,7 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
|
|
|
131
131
|
* published entries, and Related posts blocks (AGL-582) against the routed
|
|
132
132
|
* entry. Fetches lazily — screens without the blocks cost nothing — and
|
|
133
133
|
* fails open on lookup errors like every other compose stage.
|
|
134
|
-
*/ async function expandCollectionEntryBlocks(hostId, nodes, collection, prefetched) {
|
|
134
|
+
*/ async function expandCollectionEntryBlocks(hostId, nodes, collection, prefetched, /** The site's zone (AGL-3237); UTC when a site has not named one. */ timeZone) {
|
|
135
135
|
const { slugs, hasRelated, hasCategories, hasSearch } = scanCollectionBlocks(nodes, collection);
|
|
136
136
|
if (!slugs.size) return nodes;
|
|
137
137
|
const sources = {};
|
|
@@ -169,7 +169,7 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
|
|
|
169
169
|
reachedBound: true
|
|
170
170
|
} : {});
|
|
171
171
|
}));
|
|
172
|
-
const expanded = Aglyn.expandCollectionEntries(nodes, sources, collection == null ? void 0 : collection.slug);
|
|
172
|
+
const expanded = Aglyn.expandCollectionEntries(nodes, sources, collection == null ? void 0 : collection.slug, timeZone);
|
|
173
173
|
const withCategories = hasCategories ? Aglyn.expandCollectionCategories(expanded, sources, /*
|
|
174
174
|
No DEFAULT collection for the pills on a routeless page (AGL-2524).
|
|
175
175
|
|
|
@@ -190,7 +190,7 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
|
|
|
190
190
|
*/ (collection == null ? void 0 : collection.routeless) ? undefined : collection == null ? void 0 : collection.slug, collection == null ? void 0 : collection.categorySlug) : expanded;
|
|
191
191
|
const withSearch = hasSearch ? Aglyn.expandCollectionSearch(withCategories, sources, collection == null ? void 0 : collection.slug) : withCategories;
|
|
192
192
|
if (!hasRelated || !(collection == null ? void 0 : collection.entry)) return withSearch;
|
|
193
|
-
return Aglyn.expandCollectionRelated(withSearch, sources[collection.slug], collection.entry);
|
|
193
|
+
return Aglyn.expandCollectionRelated(withSearch, sources[collection.slug], collection.entry, timeZone);
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
196
|
* Shared post-version composition (AGL-551, extracted from
|
|
@@ -389,14 +389,14 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
|
|
|
389
389
|
// Collection entries blocks (AGL-551) expand alongside repeatables:
|
|
390
390
|
// per-entry {{entry.*}} tokens substitute inside the clones here, while
|
|
391
391
|
// page-level tokens wait for resolveNamedTokens below.
|
|
392
|
-
const withEntries = await expandCollectionEntryBlocks(hostId, repeated, options.collection, prefetchedSources);
|
|
392
|
+
const withEntries = await expandCollectionEntryBlocks(hostId, repeated, options.collection, prefetchedSources, options.timeZone);
|
|
393
393
|
// Entry Meta blocks (AGL-1385): fill in the routed entry's date/category/
|
|
394
394
|
// tags. Needs no source fetch — the routed entry and its taxonomy are
|
|
395
395
|
// already in hand — so it sits outside `expandCollectionEntryBlocks`, which
|
|
396
396
|
// returns early when no block asks for a collection read. AFTER it, so the
|
|
397
397
|
// per-entry clones it just produced already carry their own resolved values
|
|
398
398
|
// and are skipped.
|
|
399
|
-
const withEntryMeta = Aglyn.expandCollectionEntryMeta(withEntries, (_options_collection = options.collection) == null ? void 0 : _options_collection.entry, (_options_collection1 = options.collection) == null ? void 0 : _options_collection1.categories);
|
|
399
|
+
const withEntryMeta = Aglyn.expandCollectionEntryMeta(withEntries, (_options_collection = options.collection) == null ? void 0 : _options_collection.entry, (_options_collection1 = options.collection) == null ? void 0 : _options_collection1.categories, options.timeZone);
|
|
400
400
|
// Entry Author cards (AGL-2486): the same fill, one block over. Its values
|
|
401
401
|
// come off the author RECORD the routed entry resolved to, which the
|
|
402
402
|
// collection read has already attached, so this costs nothing either.
|
|
@@ -515,7 +515,9 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
|
|
|
515
515
|
* it exists rather than at the point we decide to drop it. The real error
|
|
516
516
|
* still propagates, from the `await versionPromise` below, exactly where it
|
|
517
517
|
* did when this function awaited the version directly.
|
|
518
|
-
*/ const composed = composeNodesWithChrome({
|
|
518
|
+
*/ const composed = composeNodesWithChrome(_extends({}, options.timeZone ? {
|
|
519
|
+
timeZone: options.timeZone
|
|
520
|
+
} : {}, {
|
|
519
521
|
hostId,
|
|
520
522
|
// Version-first (key-present wins, null = explicitly no layout), screen
|
|
521
523
|
// fallback — resolved as a promise so only the layout-chain walk waits on
|
|
@@ -538,7 +540,7 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
|
|
|
538
540
|
collection: options.collection,
|
|
539
541
|
host: options.host,
|
|
540
542
|
socialImages: options.socialImages
|
|
541
|
-
});
|
|
543
|
+
}));
|
|
542
544
|
void composed.catch(()=>undefined);
|
|
543
545
|
const versionRes = await versionPromise;
|
|
544
546
|
if (versionRes.error || !versionRes.version) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-screen-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\n// By path: the overlay is server-only, and every `@aglyn/aglyn` barrel\n// re-exports `app-utils/server` into published pages.\nimport {\n applyMediaAssetFacts,\n mediaAssetRefs,\n} from '@aglyn/aglyn/app-utils/media-asset-facts'\nimport applyDuePublishSchedule from './apply-publish-schedule'\nimport getComponents from './get-components'\nimport getDatasets from './get-datasets'\nimport getForms from './get-forms'\nimport getMediaAssetFacts from './get-media-asset-facts'\nimport {\n getPublishedCollectionSource,\n type PublishedCollectionSource,\n} from './get-collection-content'\nimport getPluginInstalls from './get-plugin-installs'\nimport getVariables, { getFunctions, getWorkflows } from './get-variables'\nimport getPublishedLayoutVersion from './get-layout-version'\nimport getScreenVersion from './get-screen-version'\nimport {\n type ComposeSocialImages,\n socialImageAssetFacts,\n socialImageRefs,\n} from './social-image-facts'\nimport { stampFormDatasetBindings } from './stamp-form-dataset-bindings'\n\n/**\n * Content-collection context for a compose (AGL-551): the collection the\n * route resolved (list/entry template screens). `entries` rides along when\n * the route already fetched them (list pages); blocks bound to other\n * collections — or to this one when `entries` is absent — fetch on demand.\n */\nexport interface ComposeCollectionContext {\n slug: string\n entries?: Aglyn.CollectionEntryRecord[]\n /**\n * The entry being rendered (AGL-582, entry-template screens / entry\n * fallback) — the Related posts block resolves against it.\n */\n entry?: Aglyn.CollectionEntryRecord | null\n /**\n * The routed collection's category taxonomy (AGL-582): entry\n * `categoryId`s resolve to display names against it during expansion.\n */\n categories?: Aglyn.CollectionCategory[]\n /**\n * The list page the URL asked for (AGL-1321). Fills in an entries block\n * that declares `perPage` but no `page` — design time cannot know which\n * page a visitor is on.\n */\n page?: number\n /**\n * The category segment the URL filtered on (AGL-1321); marks the current\n * pill in a Category Pills block. `entries` arrives already filtered.\n */\n categorySlug?: string\n /**\n * Whether the read behind `entries` stopped at its `.limit()` (AGL-1516).\n * Travels with the entries because the search boxes downstream have to say\n * what they actually searched, and — with the liveness gate and the route's\n * category filter both in between — `entries.length` no longer tells them.\n */\n entriesReachedBound?: boolean\n /**\n * Where `entries` begins in the collection's own order (AGL-3213).\n *\n * Zero for every listing served from the cached read. A page past that\n * read's bound carries only its own ten entries, and the block windows\n * `[(page - 1) * perPage, …)` — so without this it would slice from an\n * offset its own array never reaches and render an empty listing.\n */\n windowStart?: number\n /**\n * Whether {@link slug} is a cache KEY rather than an address (AGL-2524).\n *\n * The author page mixes collections, and the compose pipeline keys entry\n * sources by collection slug — so it hands over a synthetic one\n * (`AUTHOR_ENTRIES_SOURCE_SLUG`) with its entries already in hand. That is\n * harmless for the entries block, whose every row carries its OWN\n * `collectionSlug` and builds `entry.url` from it, and for the search box,\n * whose index is built the same way.\n *\n * It is NOT harmless for anything that builds a URL from the source's slug\n * itself. Set this and such a block resolves nothing rather than pointing\n * readers at `/{synthetic}/…`.\n */\n routeless?: boolean\n}\n\ninterface CollectionBlockScan {\n slugs: Set<string>\n hasRelated: boolean\n hasCategories: boolean\n hasSearch: boolean\n}\n\n/**\n * Which collections does this tree ask for (AGL-1152)?\n *\n * Extracted so the PREFETCH below and the expansion that consumes it read the\n * tree through one function rather than two copies of the same predicate. A\n * divergence between them is not a type error — it is a slug prefetched and\n * never awaited, or (worse) a slug the prefetch missed that then pays the full\n * serial read anyway — so there is deliberately no second implementation to\n * drift.\n */\nfunction scanCollectionBlocks(\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): CollectionBlockScan {\n const slugs = new Set<string>()\n let hasRelated = false\n let hasCategories = false\n let hasSearch = false\n for (const node of Object.values(nodes)) {\n if (node?.componentId === Aglyn.COLLECTION_ENTRIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) slugs.add(slug)\n }\n // Category pills (AGL-1321) need only the taxonomy, but it rides on the\n // same source, so they count as a reason to resolve one.\n if (node?.componentId === Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasCategories = true\n slugs.add(slug)\n }\n }\n // The standalone toolbar search box (AGL-1516, Figma 494:1220) needs the\n // collection's entries to index, and rides the same source the listing\n // beside it was built from — one read, one answer.\n if (node?.componentId === Aglyn.COLLECTION_SEARCH_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasSearch = true\n slugs.add(slug)\n }\n }\n // Related posts (AGL-582) always resolve against the ROUTED collection\n // — they only mean something with a current entry in context.\n if (\n node?.componentId === Aglyn.COLLECTION_RELATED_COMPONENT_ID &&\n collection?.slug &&\n collection.entry\n ) {\n hasRelated = true\n slugs.add(collection.slug)\n }\n }\n return { slugs, hasRelated, hasCategories, hasSearch }\n}\n\n/**\n * Issue the collection reads AS SOON AS THE SCREEN NODES EXIST (AGL-1152).\n *\n * `getPublishedCollectionSource` is three SEQUENTIAL round trips —\n * `findContentCollection`, then `listLiveEntries`, then `attachEntryAuthors`\n * — and until this existed it ran after the chrome bundle had already been\n * awaited, so a page carrying a Collection entries block paid the whole thing\n * as a serial tail on the compose phase. Measured on the tenant: a page with\n * no collection block composes in ~20 ms, one with a block in ~572 ms, and the\n * tree work itself accounts for under 2 ms of that at 50 entries. The gap is\n * this read, waiting for reads it shares nothing with.\n *\n * Same shape as `screenDatasetsPromise` directly below, and the same caveat\n * applies: the SCREEN's own nodes are a fast path, NOT the correctness gate. A\n * collection block can arrive from a layout or a grafted reusable component,\n * neither of which exists yet at this point, so the real scan still runs\n * against the composed tree and still fetches anything this missed.\n *\n * The ROUTED collection is deliberately excluded when its entries are already\n * in hand: the expansion below answers that slug from `collection.entries`\n * without reading at all, so prefetching it would buy a read nobody awaits.\n */\nfunction prefetchCollectionSources(\n hostId: string,\n screenNodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): Record<string, Promise<PublishedCollectionSource>> {\n const prefetched: Record<\n string,\n Promise<PublishedCollectionSource>\n > = {}\n for (const slug of scanCollectionBlocks(screenNodes, collection).slugs) {\n if (slug === collection?.slug && collection.entries) continue\n const pending = getPublishedCollectionSource({\n hostId,\n collectionSlug: slug,\n })\n // Marked handled the moment it exists, for the reason `composed` is in\n // `composeScreenNodes`: a tree that turns out not to need this slug never\n // awaits it, and an unawaited rejection takes the process down rather than\n // failing this one read. The real await below still sees the rejection.\n void pending.catch(() => undefined)\n prefetched[slug] = pending\n }\n return prefetched\n}\n\n/**\n * Expands Collection entries blocks (AGL-551) against their collections'\n * published entries, and Related posts blocks (AGL-582) against the routed\n * entry. Fetches lazily — screens without the blocks cost nothing — and\n * fails open on lookup errors like every other compose stage.\n */\nasync function expandCollectionEntryBlocks(\n hostId: string,\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n prefetched?: Record<string, Promise<PublishedCollectionSource>>,\n): Promise<Record<string, any>> {\n const { slugs, hasRelated, hasCategories, hasSearch } = scanCollectionBlocks(\n nodes,\n collection,\n )\n if (!slugs.size) return nodes\n const sources: Record<string, Aglyn.CollectionEntriesSource> = {}\n await Promise.all(\n [...slugs].map(async (slug) => {\n // The routed collection rides its already-fetched entries +\n // categories (AGL-582); other collections fetch both on demand.\n if (slug === collection?.slug && collection.entries) {\n sources[slug] = {\n slug,\n entries: collection.entries,\n categories: collection.categories,\n // Only the ROUTED collection carries the URL's page (AGL-1321) — a\n // block bound to another collection must not inherit it.\n ...(collection.page ? { page: collection.page } : {}),\n // ...nor its bound (AGL-1516): this is a fact about the read the\n // route already performed, and it describes only that collection.\n ...(collection.entriesReachedBound ? { reachedBound: true } : {}),\n // ...nor where its window starts (AGL-3213), for the same reason:\n // it is a fact about the read this route performed.\n ...(collection.windowStart\n ? { windowStart: collection.windowStart }\n : {}),\n }\n return\n }\n // The prefetch when this slug was visible on the screen's own nodes;\n // a live read when it only appeared after layout/component grafting.\n const fetched = await (prefetched?.[slug] ??\n getPublishedCollectionSource({ hostId, collectionSlug: slug }))\n sources[slug] = {\n slug,\n entries: fetched.entries,\n categories:\n slug === collection?.slug && collection.categories\n ? collection.categories\n : fetched.categories,\n ...(fetched.reachedBound ? { reachedBound: true } : {}),\n }\n }),\n )\n const expanded = Aglyn.expandCollectionEntries(\n nodes,\n sources,\n collection?.slug,\n )\n const withCategories = hasCategories\n ? Aglyn.expandCollectionCategories(\n expanded,\n sources,\n /*\n No DEFAULT collection for the pills on a routeless page (AGL-2524).\n\n Category pills are the one block that builds its links from the\n SOURCE's slug — `/{slug}/category/{x}` — because a category is a\n filter on one collection's own listing. On the author page that slug\n is synthetic, so an unbound pills block stamped links to a route that\n does not exist.\n\n Rendering nothing is the honest answer rather than a fallback. The\n page spans every collection, and there is no\n `/author/{slug}/category/{x}` for a pill to lead to; the only real\n destination would be some collection's listing, which drops the\n author the reader is looking at. A pills block that NAMES a\n collection is unaffected and still resolves that collection's own\n taxonomy — \"browse the blog by category\", which is a sensible thing\n to put beside an archive.\n */\n collection?.routeless ? undefined : collection?.slug,\n collection?.categorySlug,\n )\n : expanded\n const withSearch = hasSearch\n ? Aglyn.expandCollectionSearch(withCategories, sources, collection?.slug)\n : withCategories\n if (!hasRelated || !collection?.entry) return withSearch\n return Aglyn.expandCollectionRelated(\n withSearch,\n sources[collection.slug],\n collection.entry,\n )\n}\n\n/**\n * Shared post-version composition (AGL-551, extracted from\n * `composeScreenNodes`): layout chrome, reusable components, repeatables,\n * collection entries, bindings, function definitions, plugin installs,\n * named tokens, denormalize, and last the current facts of each placed image\n * and film, and of the social card the page is shared as. The screen path and\n * the collection-fallback\n * path (which has no screen doc) build identical trees through this one\n * pipeline.\n */\nexport async function composeNodesWithChrome(options: {\n hostId: string\n /**\n * The layout binding, or a PROMISE of it.\n *\n * The unresolved form exists for the same reason `screenNodes` accepts one\n * (AGL-1428): the binding may live on the version document (key-present\n * wins over the screen's), and awaiting the version before this call would\n * put every host-scoped read back behind it. Only the layout-chain walk\n * consumes the binding, so it alone waits; the rest of the chrome bundle\n * still starts immediately.\n */\n layoutId?:\n | string\n | null\n | Promise<string | null | undefined>\n /**\n * The screen's values for the properties of the layouts it renders inside\n * (AGL-2893), keyed by layout id — or a PROMISE of them, for the reason\n * `layoutId` accepts one: they live on the version document beside the\n * binding. Absent, every layout renders its properties' defaults.\n */\n layoutPropValues?:\n | Aglyn.AglynScreenVersion['layoutPropValues']\n | null\n | Promise<Aglyn.AglynScreenVersion['layoutPropValues'] | null | undefined>\n /**\n * The screen's own nodes, or a PROMISE of them (AGL-1428).\n *\n * Accepting the unresolved form is what lets `composeScreenNodes` hand the\n * version read over before it has finished, so the host-scoped reads below\n * — none of which look at these nodes — overlap it instead of queueing\n * behind it. Passing a resolved value behaves exactly as before, which is\n * what every other caller does.\n */\n screenNodes: Record<string, any> | Promise<Record<string, any>>\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * The host document, for `host.*` tokens (AGL-1022).\n *\n * Passed in rather than read here: every caller already holds it, and\n * `composeScreenNodes` was the largest single render phase before AGL-1225\n * cut it to one round trip — adding a read back would spend that win on\n * data we already have in hand.\n */\n host?: Aglyn.HostTokenSource | null\n /**\n * The social card the head shares this page as (AGL-2850). The documents of\n * the assets it names are read in the same batch as the images and films\n * the tree places.\n */\n socialImages?: ComposeSocialImages\n}): Promise<Record<string, any>> {\n const { hostId, layoutId } = options\n\n /**\n * The layout chain, innermost first (AGL-703).\n *\n * A layout may itself render inside another layout, so this walks the\n * `layoutId` pointers rather than reading one. Fetching is sequential\n * because each step's parent is only known once the previous layout\n * document is in hand — but the walk is short by construction\n * (MAX_LAYOUT_CHAIN_DEPTH) and every layout is already a cached read.\n *\n * `seen` stops a cycle from looping forever. Stored data can hold one\n * even though both the console and `canNestLayout` refuse to create it:\n * the API, a script, or a restored backup can all write a layout\n * document directly, and a render must degrade rather than hang.\n */\n const walkLayoutChain = async () => {\n const chain: Aglyn.LayoutChainEntry[] = []\n const seen = new Set<string>()\n const boundLayoutId = await layoutId\n let currentLayoutId = boundLayoutId ? String(boundLayoutId) : undefined\n while (\n currentLayoutId &&\n !seen.has(currentLayoutId) &&\n chain.length < Aglyn.MAX_LAYOUT_CHAIN_DEPTH\n ) {\n seen.add(currentLayoutId)\n const layoutRes = await getPublishedLayoutVersion({\n hostId,\n layoutId: currentLayoutId,\n })\n // The version's declared properties travel with its nodes (AGL-2893):\n // they are applied to this layout alone, with the screen's values for\n // this layout, before the screen is grafted into its slot.\n chain.push({\n layoutId: currentLayoutId,\n nodes: layoutRes?.version?.nodes as any,\n props: (layoutRes?.version as Aglyn.AglynLayoutVersion | undefined)\n ?.props,\n })\n const parentId = (layoutRes?.layout as any)?.layoutId\n currentLayoutId = parentId ? String(parentId) : undefined\n }\n return chain\n }\n\n /**\n * ONE round-trip stage instead of three (AGL-1225).\n *\n * This used to be `await layout walk` → `await getComponents` → `await\n * Promise.all([five reads])`: three sequential waits, where only the first\n * has any reason to be sequential. Every one of the other six reads takes\n * `hostId` and nothing else — none of them consumes the layout chain — so\n * they were waiting on a walk whose result they never look at.\n *\n * The walk stays internally sequential because it genuinely is: each step's\n * parent id is only known once the previous layout document is in hand. It\n * just no longer gates anything else. The critical path becomes the walk\n * alone rather than walk + components + bulk.\n *\n * Measured budget that motivated this (production, `/product/besigner`):\n * `composeScreenNodes` was the largest single phase at 1577 ms cold and\n * consistently ~1.4-1.6 s warm too, so this is not cold-start cost. The\n * existing `AGL-1152:render` timing line reports `composeScreenNodes` as a\n * phase, so the effect of this shows up there directly — no new\n * instrumentation, and a regression would be visible in the same place.\n */\n /**\n * Does the SCREEN itself repeat over a dataset (AGL-1440)?\n *\n * Asked as soon as the nodes are in hand — which since AGL-1428 is after\n * the fan-out has been ISSUED rather than before — so the overwhelmingly\n * common case still keeps the AGL-1225 shape: a page that repeats almost\n * always says so on its own document, and its datasets read goes out\n * alongside the remaining chrome reads instead of after them.\n *\n * It is deliberately NOT the correctness gate — a repeatable can arrive from\n * a layout or a grafted reusable component, neither of which exists yet. The\n * gate is re-asked against the composed tree after grafting, which is the\n * exact input `expandRepeatables` reads.\n */\n /*\n * ISSUE THE HOST-SCOPED READS FIRST, THEN AWAIT THE NODES (AGL-1428).\n *\n * Every read in this bundle is keyed by `hostId` (and `layoutId`) alone —\n * none of them reads `screenNodes` — so starting them before the nodes are\n * in hand lets `composeScreenNodes`' version read overlap them rather than\n * run ahead of them. `Promise.all` is created BEFORE the `await` below on\n * purpose: that is the line that makes the two independent, and moving the\n * await above it silently gives the whole saving back.\n *\n * `getDatasets` is the one read that genuinely depends on the nodes, so it\n * cannot join the bundle. It does not have to wait for the bundle either —\n * it is issued the moment the nodes resolve and awaited after, so it still\n * overlaps whatever is left of the chrome reads instead of costing the\n * extra serial round trip that dropping it from the batch would imply.\n */\n const chromeBundle = Promise.all([\n walkLayoutChain(),\n getComponents({ hostId }),\n // Host variable + function bindings (AGL-91/93): {{name}} and\n // {{fn:name(args)}} in string props resolve to values; unknown tokens\n // and failed runs stay literal.\n Promise.all([\n getVariables({ hostId }),\n getFunctions({ hostId }),\n getWorkflows({ hostId }),\n getPluginInstalls({ hostId }),\n ]),\n ])\n const screenNodes = await options.screenNodes\n const screenDatasetKeys = Aglyn.repeatDatasetKeys(screenNodes)\n const screenDatasetsPromise = screenDatasetKeys.length\n ? getDatasets({ hostId, keys: screenDatasetKeys })\n : undefined\n // Does the SCREEN itself place a form entity? Gated and re-asked exactly\n // like the datasets read beside it (AGL-1440): most pages carry no form, and\n // the ones that do usually say so on their own document, so the read goes\n // out here alongside the chrome reads instead of as a serial tail. It is not\n // the correctness gate — a placed form can arrive from a layout or a grafted\n // component — so the composed tree is asked again below.\n const screenFormsPromise = Aglyn.placesFormEntity(screenNodes)\n ? getForms({ hostId })\n : undefined\n // Issued HERE, beside the datasets read and before the chrome bundle is\n // awaited, so the collection read overlaps it instead of trailing it.\n const prefetchedSources = prefetchCollectionSources(\n hostId,\n screenNodes,\n options.collection,\n )\n const [layoutChain, componentsRes, bulk] = await chromeBundle\n const [rawVariables, functions, workflows, pluginInstalls] = bulk\n const screenDatasets = await screenDatasetsPromise\n // Settled by now: it is read off the same version document the layout\n // binding the walk above waited on came from.\n const layoutPropValues = await options.layoutPropValues\n\n const composedNodes = Aglyn.composeLayoutChainWithProps(\n layoutChain as any,\n screenNodes as any,\n layoutPropValues,\n )\n const graftedComponents = Aglyn.composeReusableComponentNodes(\n composedNodes as any,\n componentsRes.definitions as any,\n )\n /*\n * PLACED FORMS RESOLVE AGAINST THEIR ENTITY (`docs/specs/reusable-forms.md`).\n *\n * A form node bound to `hosts/{hostId}/forms/{formId}` renders that entity's\n * published design, so a form is edited once and every page placing it\n * follows. Without this the entity's tree was written on every publish and\n * read by nothing: the fields had to be redrawn per page, and the two copies\n * diverged the moment either was touched.\n *\n * The gate is the COMPONENT-grafted tree, not the screen's own nodes, for\n * the reason the repeatables gate below states: a form placed inside a\n * layout or a shared component does not exist in `screenNodes`, and a page\n * that renders one would silently keep its stale inline copy.\n *\n * The second graft re-runs the component expansion deliberately. Instances\n * already expanded are skipped by their own prefix, so the repeat costs a\n * scan, and passing BOTH placement kinds is what expands a reusable\n * component nested inside a form's design — which the first pass could not\n * have seen, because that subtree was not in the tree yet.\n */\n const forms =\n (await screenFormsPromise)?.forms ??\n (Aglyn.placesFormEntity(graftedComponents as any)\n ? (await getForms({ hostId })).forms\n : undefined)\n const grafted = forms\n ? Aglyn.composeReusableComponentNodes(\n graftedComponents as any,\n componentsRes.definitions as any,\n [Aglyn.placedFormPlacement(forms as any)],\n )\n : graftedComponents\n // Computed variables (AGL-129): workflow-backed values resolve once per\n // compose; failures keep each variable's stored fallback.\n const variables = Aglyn.resolveComputedVariables(\n rawVariables,\n functions,\n workflows,\n )\n // Repeatables (AGL-103) expand after grafting (so they work inside\n // reusable components) and before bindings (so {{name}} tokens inside\n // cloned items still resolve).\n //\n // Only the datasets this tree repeats over are read (AGL-1440), and the tree\n // asked is the composed one — after grafting — because that is the map the\n // expansion reads: a repeatable living in a layout or a reusable component\n // is invisible in `screenNodes`, and reading only the screen's keys would\n // silently render one template row where the author put a list. The\n // screen's own keys were issued beside the chrome reads above; a key only a\n // layout or a component adds is read here, for that key alone.\n const datasetKeys = Aglyn.repeatDatasetKeys(grafted as any)\n const unreadDatasetKeys = datasetKeys.filter(\n (key) => !screenDatasetKeys.includes(key),\n )\n const datasets = unreadDatasetKeys.length\n ? {\n ...screenDatasets,\n ...(await getDatasets({ hostId, keys: unreadDatasetKeys })),\n }\n : screenDatasets\n const repeated = Aglyn.expandRepeatables(grafted as any, datasets)\n // Collection entries blocks (AGL-551) expand alongside repeatables:\n // per-entry {{entry.*}} tokens substitute inside the clones here, while\n // page-level tokens wait for resolveNamedTokens below.\n const withEntries = await expandCollectionEntryBlocks(\n hostId,\n repeated,\n options.collection,\n prefetchedSources,\n )\n // Entry Meta blocks (AGL-1385): fill in the routed entry's date/category/\n // tags. Needs no source fetch — the routed entry and its taxonomy are\n // already in hand — so it sits outside `expandCollectionEntryBlocks`, which\n // returns early when no block asks for a collection read. AFTER it, so the\n // per-entry clones it just produced already carry their own resolved values\n // and are skipped.\n const withEntryMeta = Aglyn.expandCollectionEntryMeta(\n withEntries as any,\n options.collection?.entry,\n options.collection?.categories,\n )\n // Entry Author cards (AGL-2486): the same fill, one block over. Its values\n // come off the author RECORD the routed entry resolved to, which the\n // collection read has already attached, so this costs nothing either.\n const withEntryAuthor = Aglyn.expandCollectionEntryAuthor(\n withEntryMeta as any,\n options.collection?.entry,\n )\n const bound = Aglyn.resolveNodesBindings(\n withEntryAuthor as any,\n variables,\n functions,\n )\n // Host variables (AGL-1022): `{{host.*}}` resolves from the INSTALLING\n // site, late — at render, never at install — so a rebrand propagates to\n // every artifact that names the host instead of hard-coding it. Same\n // registry the email path uses, so a token means one thing in both.\n const withHostTokens = Aglyn.resolveNodesHostTokens(\n bound as any,\n options.host,\n )\n // Function widgets run client-side: embed their definitions (AGL-93), and\n // beside each the site variables that function reads and no others\n // (AGL-3202).\n const withFunctions = Aglyn.attachFunctionDefinitions(\n withHostTokens,\n functions,\n variables,\n )\n // Marketplace plugins (AGL-45): stamp each marketplacePlugin node with its\n // pinned install (version/sha256/capabilities) + kill-switch state.\n const nodes = Aglyn.attachPluginInstalls(withFunctions, pluginInstalls)\n // Entry-template tokens (AGL-105): {{entry.*}} from the rendered entry.\n const finalNodes = Aglyn.resolveNamedTokens(nodes as any, options.tokens)\n // The document's one `main` landmark (AGL-2486). LAST, so it reads the tree\n // the page actually ships — a slot grafted from a layout chain, an element\n // an author chose — rather than the screen as stored.\n const withLandmark = Aglyn.stampDocumentLandmark(finalNodes as any)\n // Each form's dataset binding, signed so the submit route can trust it\n // (AGL-2773). Read off THIS tree, the one the page ships, so a form grafted\n // from a layout, a component or a form entity is signed as it renders; and\n // after every stage that rewrites props, so nothing changes what the\n // signature covers.\n const withFormBindings = stampFormDatasetBindings(withLandmark, hostId)\n const denormalized = Aglyn.canvas.processNodesToDenormalized(\n withFormBindings as any,\n )\n /*\n * WHAT EACH PLACED IMAGE AND FILM IS NOW (AGL-2807, AGL-2833).\n *\n * An image's pixel pair and a film's length, shape and poster come from the\n * asset's DAM document as it is now, not as it was when it was picked. LAST,\n * on the tree the page ships, because an asset can arrive from any stage\n * above: a layout, a component, a form, a repeated row, a collection entry,\n * a binding.\n *\n * ONE read for all of them, issued once the tree is final rather than beside\n * the chrome reads. A read issued there could only cover the screen's own\n * placements, and nearly every layout places an image the screen does not\n * (a logo, a footer mark), so it would buy a second read on almost every\n * page. The late read costs its own round trip after the chrome reads, paid\n * each time a page is composed, which for a published page is its ISR\n * regeneration. A tree with no library asset, on a page whose social card\n * names none, issues none.\n *\n * THE SOCIAL CARD'S ASSETS JOIN THE SAME READ (AGL-2850), ahead of the\n * placements. A placement the cap leaves out keeps its pick-time pair, which\n * for an image is a reservation the decoded picture corrects. Nothing\n * corrects a card's pair, because a crawler lays the card out from it before\n * fetching the image, and a card names at most three documents.\n */\n const socialImages = options.socialImages\n const cardRefs = socialImages ? socialImageRefs(socialImages.images) : []\n const refs = mediaAssetRefs(denormalized)\n if (!refs.length && !cardRefs.length) return denormalized\n const facts = await getMediaAssetFacts({\n hostId,\n refs: [...cardRefs, ...refs],\n })\n if (socialImages) {\n const cardFacts = socialImageAssetFacts(socialImages.images, facts)\n if (cardFacts) socialImages.onFacts(cardFacts)\n }\n return applyMediaAssetFacts(denormalized, facts)\n}\n\n/**\n * Full published-render composition for one screen (extracted for AGL-87 so\n * the SSG path and the password-unlock API build identical trees): applies\n * a due publish schedule, loads the version, composes the shared layout\n * chrome, grafts reusable components, and denormalizes.\n */\nexport async function composeScreenNodes(options: {\n hostId: string\n screenId: string\n screen: Aglyn.AglynScreen\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * Compose a specific version instead of the published one (AGL-253):\n * experiment variants point at versions; schedules don't apply.\n */\n versionId?: string\n /** The host document, for `host.*` tokens (AGL-1022). */\n host?: Aglyn.HostTokenSource | null\n /** The social card the head shares this page as (AGL-2850). */\n socialImages?: ComposeSocialImages\n}): Promise<Record<string, any> | null> {\n const { hostId, screenId, screen } = options\n\n const effectiveVersionId = options.versionId\n ? null\n : await applyDuePublishSchedule({\n hostId,\n collectionName: 'screens',\n docId: screenId,\n parent: screen,\n })\n const versionId = (options.versionId ??\n effectiveVersionId ??\n screen.versionId) as string\n\n /*\n * OVERLAP THE VERSION READ WITH THE CHROME BUNDLE (AGL-1428).\n *\n * `composeNodesWithChrome`'s reads are keyed by `hostId`/`layoutId`, both\n * of which are in hand here, so the version read no longer has to finish\n * before they start. Handing the promise over instead of the resolved\n * value is the whole change: it is `composeNodesWithChrome` that decides\n * when it actually needs the nodes.\n *\n * The `versionId`-less screen still exits BEFORE anything is issued, so a\n * screen that has never been published costs no reads. What remains is the\n * narrow case of a `versionId` that points at a missing or unreadable\n * version document — a data-integrity fault rather than a routing outcome\n * — and that one now pays for a chrome bundle it discards. That is the\n * deliberate trade: one wasted bundle on a broken screen, against the\n * version read hiding under the chrome reads on every render that works.\n */\n if (!versionId) return null\n\n const versionPromise = getScreenVersion({ hostId, screenId, versionId })\n\n /*\n * NEITHER DERIVED PROMISE MAY REJECT ON ITS OWN.\n *\n * `composed` is discarded on every failure path below, and a rejected\n * promise nobody awaited is an unhandled rejection — which in Node takes\n * the whole render process down instead of letting this 404. So the nodes\n * handed to the compose absorb the failure (an empty tree it will never be\n * asked for), and `composed` gets a rejection handler attached the moment\n * it exists rather than at the point we decide to drop it. The real error\n * still propagates, from the `await versionPromise` below, exactly where it\n * did when this function awaited the version directly.\n */\n const composed = composeNodesWithChrome({\n hostId,\n // Version-first (key-present wins, null = explicitly no layout), screen\n // fallback — resolved as a promise so only the layout-chain walk waits on\n // the version read; the rest of the chrome bundle keeps the AGL-1428\n // overlap. A failed version read falls back to the screen binding; the\n // whole compose is discarded on that path anyway.\n layoutId: versionPromise.then(\n (res) =>\n res.version && 'layoutId' in res.version\n ? ((res.version as Aglyn.AglynScreenVersion).layoutId as\n | string\n | null)\n : (screen.layoutId as string | undefined),\n () => screen.layoutId as string | undefined,\n ),\n // The screen's values for its layouts' properties (AGL-2893), beside the\n // binding on the same document; a failed read renders the defaults.\n layoutPropValues: versionPromise.then(\n (res) =>\n (res.version as Aglyn.AglynScreenVersion | undefined)?.layoutPropValues,\n () => undefined,\n ),\n screenNodes: versionPromise.then(\n (res) => (res.version?.nodes ?? {}) as any,\n () => ({}) as any,\n ),\n tokens: options.tokens,\n collection: options.collection,\n host: options.host,\n socialImages: options.socialImages,\n })\n void composed.catch(() => undefined)\n\n const versionRes = await versionPromise\n if (versionRes.error || !versionRes.version) return null\n\n return composed\n}\n\nexport default composeScreenNodes\n"],"names":["Aglyn","applyMediaAssetFacts","mediaAssetRefs","applyDuePublishSchedule","getComponents","getDatasets","getForms","getMediaAssetFacts","getPublishedCollectionSource","getPluginInstalls","getVariables","getFunctions","getWorkflows","getPublishedLayoutVersion","getScreenVersion","socialImageAssetFacts","socialImageRefs","stampFormDatasetBindings","scanCollectionBlocks","nodes","collection","slugs","Set","hasRelated","hasCategories","hasSearch","node","Object","values","componentId","COLLECTION_ENTRIES_COMPONENT_ID","slug","String","props","collectionSlug","trim","add","COLLECTION_CATEGORIES_COMPONENT_ID","COLLECTION_SEARCH_COMPONENT_ID","COLLECTION_RELATED_COMPONENT_ID","entry","prefetchCollectionSources","hostId","screenNodes","prefetched","entries","pending","catch","undefined","expandCollectionEntryBlocks","size","sources","Promise","all","map","categories","page","entriesReachedBound","reachedBound","windowStart","fetched","expanded","expandCollectionEntries","withCategories","expandCollectionCategories","routeless","categorySlug","withSearch","expandCollectionSearch","expandCollectionRelated","composeNodesWithChrome","options","layoutId","walkLayoutChain","chain","seen","boundLayoutId","currentLayoutId","has","length","MAX_LAYOUT_CHAIN_DEPTH","layoutRes","push","version","parentId","layout","chromeBundle","screenDatasetKeys","repeatDatasetKeys","screenDatasetsPromise","keys","screenFormsPromise","placesFormEntity","prefetchedSources","layoutChain","componentsRes","bulk","rawVariables","functions","workflows","pluginInstalls","screenDatasets","layoutPropValues","composedNodes","composeLayoutChainWithProps","graftedComponents","composeReusableComponentNodes","definitions","forms","grafted","placedFormPlacement","variables","resolveComputedVariables","datasetKeys","unreadDatasetKeys","filter","key","includes","datasets","repeated","expandRepeatables","withEntries","withEntryMeta","expandCollectionEntryMeta","withEntryAuthor","expandCollectionEntryAuthor","bound","resolveNodesBindings","withHostTokens","resolveNodesHostTokens","host","withFunctions","attachFunctionDefinitions","attachPluginInstalls","finalNodes","resolveNamedTokens","tokens","withLandmark","stampDocumentLandmark","withFormBindings","denormalized","canvas","processNodesToDenormalized","socialImages","cardRefs","images","refs","facts","cardFacts","onFacts","composeScreenNodes","screenId","screen","effectiveVersionId","versionId","collectionName","docId","parent","versionPromise","composed","then","res","versionRes","error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-screen-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\n// By path: the overlay is server-only, and every `@aglyn/aglyn` barrel\n// re-exports `app-utils/server` into published pages.\nimport {\n applyMediaAssetFacts,\n mediaAssetRefs,\n} from '@aglyn/aglyn/app-utils/media-asset-facts'\nimport applyDuePublishSchedule from './apply-publish-schedule'\nimport getComponents from './get-components'\nimport getDatasets from './get-datasets'\nimport getForms from './get-forms'\nimport getMediaAssetFacts from './get-media-asset-facts'\nimport {\n getPublishedCollectionSource,\n type PublishedCollectionSource,\n} from './get-collection-content'\nimport getPluginInstalls from './get-plugin-installs'\nimport getVariables, { getFunctions, getWorkflows } from './get-variables'\nimport getPublishedLayoutVersion from './get-layout-version'\nimport getScreenVersion from './get-screen-version'\nimport {\n type ComposeSocialImages,\n socialImageAssetFacts,\n socialImageRefs,\n} from './social-image-facts'\nimport { stampFormDatasetBindings } from './stamp-form-dataset-bindings'\n\n/**\n * Content-collection context for a compose (AGL-551): the collection the\n * route resolved (list/entry template screens). `entries` rides along when\n * the route already fetched them (list pages); blocks bound to other\n * collections — or to this one when `entries` is absent — fetch on demand.\n */\nexport interface ComposeCollectionContext {\n slug: string\n entries?: Aglyn.CollectionEntryRecord[]\n /**\n * The entry being rendered (AGL-582, entry-template screens / entry\n * fallback) — the Related posts block resolves against it.\n */\n entry?: Aglyn.CollectionEntryRecord | null\n /**\n * The routed collection's category taxonomy (AGL-582): entry\n * `categoryId`s resolve to display names against it during expansion.\n */\n categories?: Aglyn.CollectionCategory[]\n /**\n * The list page the URL asked for (AGL-1321). Fills in an entries block\n * that declares `perPage` but no `page` — design time cannot know which\n * page a visitor is on.\n */\n page?: number\n /**\n * The category segment the URL filtered on (AGL-1321); marks the current\n * pill in a Category Pills block. `entries` arrives already filtered.\n */\n categorySlug?: string\n /**\n * Whether the read behind `entries` stopped at its `.limit()` (AGL-1516).\n * Travels with the entries because the search boxes downstream have to say\n * what they actually searched, and — with the liveness gate and the route's\n * category filter both in between — `entries.length` no longer tells them.\n */\n entriesReachedBound?: boolean\n /**\n * Where `entries` begins in the collection's own order (AGL-3213).\n *\n * Zero for every listing served from the cached read. A page past that\n * read's bound carries only its own ten entries, and the block windows\n * `[(page - 1) * perPage, …)` — so without this it would slice from an\n * offset its own array never reaches and render an empty listing.\n */\n windowStart?: number\n /**\n * Whether {@link slug} is a cache KEY rather than an address (AGL-2524).\n *\n * The author page mixes collections, and the compose pipeline keys entry\n * sources by collection slug — so it hands over a synthetic one\n * (`AUTHOR_ENTRIES_SOURCE_SLUG`) with its entries already in hand. That is\n * harmless for the entries block, whose every row carries its OWN\n * `collectionSlug` and builds `entry.url` from it, and for the search box,\n * whose index is built the same way.\n *\n * It is NOT harmless for anything that builds a URL from the source's slug\n * itself. Set this and such a block resolves nothing rather than pointing\n * readers at `/{synthetic}/…`.\n */\n routeless?: boolean\n}\n\ninterface CollectionBlockScan {\n slugs: Set<string>\n hasRelated: boolean\n hasCategories: boolean\n hasSearch: boolean\n}\n\n/**\n * Which collections does this tree ask for (AGL-1152)?\n *\n * Extracted so the PREFETCH below and the expansion that consumes it read the\n * tree through one function rather than two copies of the same predicate. A\n * divergence between them is not a type error — it is a slug prefetched and\n * never awaited, or (worse) a slug the prefetch missed that then pays the full\n * serial read anyway — so there is deliberately no second implementation to\n * drift.\n */\nfunction scanCollectionBlocks(\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): CollectionBlockScan {\n const slugs = new Set<string>()\n let hasRelated = false\n let hasCategories = false\n let hasSearch = false\n for (const node of Object.values(nodes)) {\n if (node?.componentId === Aglyn.COLLECTION_ENTRIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) slugs.add(slug)\n }\n // Category pills (AGL-1321) need only the taxonomy, but it rides on the\n // same source, so they count as a reason to resolve one.\n if (node?.componentId === Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasCategories = true\n slugs.add(slug)\n }\n }\n // The standalone toolbar search box (AGL-1516, Figma 494:1220) needs the\n // collection's entries to index, and rides the same source the listing\n // beside it was built from — one read, one answer.\n if (node?.componentId === Aglyn.COLLECTION_SEARCH_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasSearch = true\n slugs.add(slug)\n }\n }\n // Related posts (AGL-582) always resolve against the ROUTED collection\n // — they only mean something with a current entry in context.\n if (\n node?.componentId === Aglyn.COLLECTION_RELATED_COMPONENT_ID &&\n collection?.slug &&\n collection.entry\n ) {\n hasRelated = true\n slugs.add(collection.slug)\n }\n }\n return { slugs, hasRelated, hasCategories, hasSearch }\n}\n\n/**\n * Issue the collection reads AS SOON AS THE SCREEN NODES EXIST (AGL-1152).\n *\n * `getPublishedCollectionSource` is three SEQUENTIAL round trips —\n * `findContentCollection`, then `listLiveEntries`, then `attachEntryAuthors`\n * — and until this existed it ran after the chrome bundle had already been\n * awaited, so a page carrying a Collection entries block paid the whole thing\n * as a serial tail on the compose phase. Measured on the tenant: a page with\n * no collection block composes in ~20 ms, one with a block in ~572 ms, and the\n * tree work itself accounts for under 2 ms of that at 50 entries. The gap is\n * this read, waiting for reads it shares nothing with.\n *\n * Same shape as `screenDatasetsPromise` directly below, and the same caveat\n * applies: the SCREEN's own nodes are a fast path, NOT the correctness gate. A\n * collection block can arrive from a layout or a grafted reusable component,\n * neither of which exists yet at this point, so the real scan still runs\n * against the composed tree and still fetches anything this missed.\n *\n * The ROUTED collection is deliberately excluded when its entries are already\n * in hand: the expansion below answers that slug from `collection.entries`\n * without reading at all, so prefetching it would buy a read nobody awaits.\n */\nfunction prefetchCollectionSources(\n hostId: string,\n screenNodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): Record<string, Promise<PublishedCollectionSource>> {\n const prefetched: Record<\n string,\n Promise<PublishedCollectionSource>\n > = {}\n for (const slug of scanCollectionBlocks(screenNodes, collection).slugs) {\n if (slug === collection?.slug && collection.entries) continue\n const pending = getPublishedCollectionSource({\n hostId,\n collectionSlug: slug,\n })\n // Marked handled the moment it exists, for the reason `composed` is in\n // `composeScreenNodes`: a tree that turns out not to need this slug never\n // awaits it, and an unawaited rejection takes the process down rather than\n // failing this one read. The real await below still sees the rejection.\n void pending.catch(() => undefined)\n prefetched[slug] = pending\n }\n return prefetched\n}\n\n/**\n * Expands Collection entries blocks (AGL-551) against their collections'\n * published entries, and Related posts blocks (AGL-582) against the routed\n * entry. Fetches lazily — screens without the blocks cost nothing — and\n * fails open on lookup errors like every other compose stage.\n */\nasync function expandCollectionEntryBlocks(\n hostId: string,\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n prefetched?: Record<string, Promise<PublishedCollectionSource>>,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Promise<Record<string, any>> {\n const { slugs, hasRelated, hasCategories, hasSearch } = scanCollectionBlocks(\n nodes,\n collection,\n )\n if (!slugs.size) return nodes\n const sources: Record<string, Aglyn.CollectionEntriesSource> = {}\n await Promise.all(\n [...slugs].map(async (slug) => {\n // The routed collection rides its already-fetched entries +\n // categories (AGL-582); other collections fetch both on demand.\n if (slug === collection?.slug && collection.entries) {\n sources[slug] = {\n slug,\n entries: collection.entries,\n categories: collection.categories,\n // Only the ROUTED collection carries the URL's page (AGL-1321) — a\n // block bound to another collection must not inherit it.\n ...(collection.page ? { page: collection.page } : {}),\n // ...nor its bound (AGL-1516): this is a fact about the read the\n // route already performed, and it describes only that collection.\n ...(collection.entriesReachedBound ? { reachedBound: true } : {}),\n // ...nor where its window starts (AGL-3213), for the same reason:\n // it is a fact about the read this route performed.\n ...(collection.windowStart\n ? { windowStart: collection.windowStart }\n : {}),\n }\n return\n }\n // The prefetch when this slug was visible on the screen's own nodes;\n // a live read when it only appeared after layout/component grafting.\n const fetched = await (prefetched?.[slug] ??\n getPublishedCollectionSource({ hostId, collectionSlug: slug }))\n sources[slug] = {\n slug,\n entries: fetched.entries,\n categories:\n slug === collection?.slug && collection.categories\n ? collection.categories\n : fetched.categories,\n ...(fetched.reachedBound ? { reachedBound: true } : {}),\n }\n }),\n )\n const expanded = Aglyn.expandCollectionEntries(\n nodes,\n sources,\n collection?.slug,\n timeZone,\n )\n const withCategories = hasCategories\n ? Aglyn.expandCollectionCategories(\n expanded,\n sources,\n /*\n No DEFAULT collection for the pills on a routeless page (AGL-2524).\n\n Category pills are the one block that builds its links from the\n SOURCE's slug — `/{slug}/category/{x}` — because a category is a\n filter on one collection's own listing. On the author page that slug\n is synthetic, so an unbound pills block stamped links to a route that\n does not exist.\n\n Rendering nothing is the honest answer rather than a fallback. The\n page spans every collection, and there is no\n `/author/{slug}/category/{x}` for a pill to lead to; the only real\n destination would be some collection's listing, which drops the\n author the reader is looking at. A pills block that NAMES a\n collection is unaffected and still resolves that collection's own\n taxonomy — \"browse the blog by category\", which is a sensible thing\n to put beside an archive.\n */\n collection?.routeless ? undefined : collection?.slug,\n collection?.categorySlug,\n )\n : expanded\n const withSearch = hasSearch\n ? Aglyn.expandCollectionSearch(withCategories, sources, collection?.slug)\n : withCategories\n if (!hasRelated || !collection?.entry) return withSearch\n return Aglyn.expandCollectionRelated(\n withSearch,\n sources[collection.slug],\n collection.entry,\n timeZone,\n )\n}\n\n/**\n * Shared post-version composition (AGL-551, extracted from\n * `composeScreenNodes`): layout chrome, reusable components, repeatables,\n * collection entries, bindings, function definitions, plugin installs,\n * named tokens, denormalize, and last the current facts of each placed image\n * and film, and of the social card the page is shared as. The screen path and\n * the collection-fallback\n * path (which has no screen doc) build identical trees through this one\n * pipeline.\n */\nexport async function composeNodesWithChrome(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of\n * the org and the host, resolved ONCE by the caller that holds both.\n *\n * A string rather than the two documents, because the value has to be\n * identical on the server render and the client re-render, and the surest\n * way to guarantee that is for only one place to decide it. Absent is UTC,\n * which is what every site rendered before this existed.\n */\n timeZone?: string\n /**\n * The layout binding, or a PROMISE of it.\n *\n * The unresolved form exists for the same reason `screenNodes` accepts one\n * (AGL-1428): the binding may live on the version document (key-present\n * wins over the screen's), and awaiting the version before this call would\n * put every host-scoped read back behind it. Only the layout-chain walk\n * consumes the binding, so it alone waits; the rest of the chrome bundle\n * still starts immediately.\n */\n layoutId?:\n | string\n | null\n | Promise<string | null | undefined>\n /**\n * The screen's values for the properties of the layouts it renders inside\n * (AGL-2893), keyed by layout id — or a PROMISE of them, for the reason\n * `layoutId` accepts one: they live on the version document beside the\n * binding. Absent, every layout renders its properties' defaults.\n */\n layoutPropValues?:\n | Aglyn.AglynScreenVersion['layoutPropValues']\n | null\n | Promise<Aglyn.AglynScreenVersion['layoutPropValues'] | null | undefined>\n /**\n * The screen's own nodes, or a PROMISE of them (AGL-1428).\n *\n * Accepting the unresolved form is what lets `composeScreenNodes` hand the\n * version read over before it has finished, so the host-scoped reads below\n * — none of which look at these nodes — overlap it instead of queueing\n * behind it. Passing a resolved value behaves exactly as before, which is\n * what every other caller does.\n */\n screenNodes: Record<string, any> | Promise<Record<string, any>>\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * The host document, for `host.*` tokens (AGL-1022).\n *\n * Passed in rather than read here: every caller already holds it, and\n * `composeScreenNodes` was the largest single render phase before AGL-1225\n * cut it to one round trip — adding a read back would spend that win on\n * data we already have in hand.\n */\n host?: Aglyn.HostTokenSource | null\n /**\n * The social card the head shares this page as (AGL-2850). The documents of\n * the assets it names are read in the same batch as the images and films\n * the tree places.\n */\n socialImages?: ComposeSocialImages\n}): Promise<Record<string, any>> {\n const { hostId, layoutId } = options\n\n /**\n * The layout chain, innermost first (AGL-703).\n *\n * A layout may itself render inside another layout, so this walks the\n * `layoutId` pointers rather than reading one. Fetching is sequential\n * because each step's parent is only known once the previous layout\n * document is in hand — but the walk is short by construction\n * (MAX_LAYOUT_CHAIN_DEPTH) and every layout is already a cached read.\n *\n * `seen` stops a cycle from looping forever. Stored data can hold one\n * even though both the console and `canNestLayout` refuse to create it:\n * the API, a script, or a restored backup can all write a layout\n * document directly, and a render must degrade rather than hang.\n */\n const walkLayoutChain = async () => {\n const chain: Aglyn.LayoutChainEntry[] = []\n const seen = new Set<string>()\n const boundLayoutId = await layoutId\n let currentLayoutId = boundLayoutId ? String(boundLayoutId) : undefined\n while (\n currentLayoutId &&\n !seen.has(currentLayoutId) &&\n chain.length < Aglyn.MAX_LAYOUT_CHAIN_DEPTH\n ) {\n seen.add(currentLayoutId)\n const layoutRes = await getPublishedLayoutVersion({\n hostId,\n layoutId: currentLayoutId,\n })\n // The version's declared properties travel with its nodes (AGL-2893):\n // they are applied to this layout alone, with the screen's values for\n // this layout, before the screen is grafted into its slot.\n chain.push({\n layoutId: currentLayoutId,\n nodes: layoutRes?.version?.nodes as any,\n props: (layoutRes?.version as Aglyn.AglynLayoutVersion | undefined)\n ?.props,\n })\n const parentId = (layoutRes?.layout as any)?.layoutId\n currentLayoutId = parentId ? String(parentId) : undefined\n }\n return chain\n }\n\n /**\n * ONE round-trip stage instead of three (AGL-1225).\n *\n * This used to be `await layout walk` → `await getComponents` → `await\n * Promise.all([five reads])`: three sequential waits, where only the first\n * has any reason to be sequential. Every one of the other six reads takes\n * `hostId` and nothing else — none of them consumes the layout chain — so\n * they were waiting on a walk whose result they never look at.\n *\n * The walk stays internally sequential because it genuinely is: each step's\n * parent id is only known once the previous layout document is in hand. It\n * just no longer gates anything else. The critical path becomes the walk\n * alone rather than walk + components + bulk.\n *\n * Measured budget that motivated this (production, `/product/besigner`):\n * `composeScreenNodes` was the largest single phase at 1577 ms cold and\n * consistently ~1.4-1.6 s warm too, so this is not cold-start cost. The\n * existing `AGL-1152:render` timing line reports `composeScreenNodes` as a\n * phase, so the effect of this shows up there directly — no new\n * instrumentation, and a regression would be visible in the same place.\n */\n /**\n * Does the SCREEN itself repeat over a dataset (AGL-1440)?\n *\n * Asked as soon as the nodes are in hand — which since AGL-1428 is after\n * the fan-out has been ISSUED rather than before — so the overwhelmingly\n * common case still keeps the AGL-1225 shape: a page that repeats almost\n * always says so on its own document, and its datasets read goes out\n * alongside the remaining chrome reads instead of after them.\n *\n * It is deliberately NOT the correctness gate — a repeatable can arrive from\n * a layout or a grafted reusable component, neither of which exists yet. The\n * gate is re-asked against the composed tree after grafting, which is the\n * exact input `expandRepeatables` reads.\n */\n /*\n * ISSUE THE HOST-SCOPED READS FIRST, THEN AWAIT THE NODES (AGL-1428).\n *\n * Every read in this bundle is keyed by `hostId` (and `layoutId`) alone —\n * none of them reads `screenNodes` — so starting them before the nodes are\n * in hand lets `composeScreenNodes`' version read overlap them rather than\n * run ahead of them. `Promise.all` is created BEFORE the `await` below on\n * purpose: that is the line that makes the two independent, and moving the\n * await above it silently gives the whole saving back.\n *\n * `getDatasets` is the one read that genuinely depends on the nodes, so it\n * cannot join the bundle. It does not have to wait for the bundle either —\n * it is issued the moment the nodes resolve and awaited after, so it still\n * overlaps whatever is left of the chrome reads instead of costing the\n * extra serial round trip that dropping it from the batch would imply.\n */\n const chromeBundle = Promise.all([\n walkLayoutChain(),\n getComponents({ hostId }),\n // Host variable + function bindings (AGL-91/93): {{name}} and\n // {{fn:name(args)}} in string props resolve to values; unknown tokens\n // and failed runs stay literal.\n Promise.all([\n getVariables({ hostId }),\n getFunctions({ hostId }),\n getWorkflows({ hostId }),\n getPluginInstalls({ hostId }),\n ]),\n ])\n const screenNodes = await options.screenNodes\n const screenDatasetKeys = Aglyn.repeatDatasetKeys(screenNodes)\n const screenDatasetsPromise = screenDatasetKeys.length\n ? getDatasets({ hostId, keys: screenDatasetKeys })\n : undefined\n // Does the SCREEN itself place a form entity? Gated and re-asked exactly\n // like the datasets read beside it (AGL-1440): most pages carry no form, and\n // the ones that do usually say so on their own document, so the read goes\n // out here alongside the chrome reads instead of as a serial tail. It is not\n // the correctness gate — a placed form can arrive from a layout or a grafted\n // component — so the composed tree is asked again below.\n const screenFormsPromise = Aglyn.placesFormEntity(screenNodes)\n ? getForms({ hostId })\n : undefined\n // Issued HERE, beside the datasets read and before the chrome bundle is\n // awaited, so the collection read overlaps it instead of trailing it.\n const prefetchedSources = prefetchCollectionSources(\n hostId,\n screenNodes,\n options.collection,\n )\n const [layoutChain, componentsRes, bulk] = await chromeBundle\n const [rawVariables, functions, workflows, pluginInstalls] = bulk\n const screenDatasets = await screenDatasetsPromise\n // Settled by now: it is read off the same version document the layout\n // binding the walk above waited on came from.\n const layoutPropValues = await options.layoutPropValues\n\n const composedNodes = Aglyn.composeLayoutChainWithProps(\n layoutChain as any,\n screenNodes as any,\n layoutPropValues,\n )\n const graftedComponents = Aglyn.composeReusableComponentNodes(\n composedNodes as any,\n componentsRes.definitions as any,\n )\n /*\n * PLACED FORMS RESOLVE AGAINST THEIR ENTITY (`docs/specs/reusable-forms.md`).\n *\n * A form node bound to `hosts/{hostId}/forms/{formId}` renders that entity's\n * published design, so a form is edited once and every page placing it\n * follows. Without this the entity's tree was written on every publish and\n * read by nothing: the fields had to be redrawn per page, and the two copies\n * diverged the moment either was touched.\n *\n * The gate is the COMPONENT-grafted tree, not the screen's own nodes, for\n * the reason the repeatables gate below states: a form placed inside a\n * layout or a shared component does not exist in `screenNodes`, and a page\n * that renders one would silently keep its stale inline copy.\n *\n * The second graft re-runs the component expansion deliberately. Instances\n * already expanded are skipped by their own prefix, so the repeat costs a\n * scan, and passing BOTH placement kinds is what expands a reusable\n * component nested inside a form's design — which the first pass could not\n * have seen, because that subtree was not in the tree yet.\n */\n const forms =\n (await screenFormsPromise)?.forms ??\n (Aglyn.placesFormEntity(graftedComponents as any)\n ? (await getForms({ hostId })).forms\n : undefined)\n const grafted = forms\n ? Aglyn.composeReusableComponentNodes(\n graftedComponents as any,\n componentsRes.definitions as any,\n [Aglyn.placedFormPlacement(forms as any)],\n )\n : graftedComponents\n // Computed variables (AGL-129): workflow-backed values resolve once per\n // compose; failures keep each variable's stored fallback.\n const variables = Aglyn.resolveComputedVariables(\n rawVariables,\n functions,\n workflows,\n )\n // Repeatables (AGL-103) expand after grafting (so they work inside\n // reusable components) and before bindings (so {{name}} tokens inside\n // cloned items still resolve).\n //\n // Only the datasets this tree repeats over are read (AGL-1440), and the tree\n // asked is the composed one — after grafting — because that is the map the\n // expansion reads: a repeatable living in a layout or a reusable component\n // is invisible in `screenNodes`, and reading only the screen's keys would\n // silently render one template row where the author put a list. The\n // screen's own keys were issued beside the chrome reads above; a key only a\n // layout or a component adds is read here, for that key alone.\n const datasetKeys = Aglyn.repeatDatasetKeys(grafted as any)\n const unreadDatasetKeys = datasetKeys.filter(\n (key) => !screenDatasetKeys.includes(key),\n )\n const datasets = unreadDatasetKeys.length\n ? {\n ...screenDatasets,\n ...(await getDatasets({ hostId, keys: unreadDatasetKeys })),\n }\n : screenDatasets\n const repeated = Aglyn.expandRepeatables(grafted as any, datasets)\n // Collection entries blocks (AGL-551) expand alongside repeatables:\n // per-entry {{entry.*}} tokens substitute inside the clones here, while\n // page-level tokens wait for resolveNamedTokens below.\n const withEntries = await expandCollectionEntryBlocks(\n hostId,\n repeated,\n options.collection,\n prefetchedSources,\n options.timeZone,\n )\n // Entry Meta blocks (AGL-1385): fill in the routed entry's date/category/\n // tags. Needs no source fetch — the routed entry and its taxonomy are\n // already in hand — so it sits outside `expandCollectionEntryBlocks`, which\n // returns early when no block asks for a collection read. AFTER it, so the\n // per-entry clones it just produced already carry their own resolved values\n // and are skipped.\n const withEntryMeta = Aglyn.expandCollectionEntryMeta(\n withEntries as any,\n options.collection?.entry,\n options.collection?.categories,\n options.timeZone,\n )\n // Entry Author cards (AGL-2486): the same fill, one block over. Its values\n // come off the author RECORD the routed entry resolved to, which the\n // collection read has already attached, so this costs nothing either.\n const withEntryAuthor = Aglyn.expandCollectionEntryAuthor(\n withEntryMeta as any,\n options.collection?.entry,\n )\n const bound = Aglyn.resolveNodesBindings(\n withEntryAuthor as any,\n variables,\n functions,\n )\n // Host variables (AGL-1022): `{{host.*}}` resolves from the INSTALLING\n // site, late — at render, never at install — so a rebrand propagates to\n // every artifact that names the host instead of hard-coding it. Same\n // registry the email path uses, so a token means one thing in both.\n const withHostTokens = Aglyn.resolveNodesHostTokens(\n bound as any,\n options.host,\n )\n // Function widgets run client-side: embed their definitions (AGL-93), and\n // beside each the site variables that function reads and no others\n // (AGL-3202).\n const withFunctions = Aglyn.attachFunctionDefinitions(\n withHostTokens,\n functions,\n variables,\n )\n // Marketplace plugins (AGL-45): stamp each marketplacePlugin node with its\n // pinned install (version/sha256/capabilities) + kill-switch state.\n const nodes = Aglyn.attachPluginInstalls(withFunctions, pluginInstalls)\n // Entry-template tokens (AGL-105): {{entry.*}} from the rendered entry.\n const finalNodes = Aglyn.resolveNamedTokens(nodes as any, options.tokens)\n // The document's one `main` landmark (AGL-2486). LAST, so it reads the tree\n // the page actually ships — a slot grafted from a layout chain, an element\n // an author chose — rather than the screen as stored.\n const withLandmark = Aglyn.stampDocumentLandmark(finalNodes as any)\n // Each form's dataset binding, signed so the submit route can trust it\n // (AGL-2773). Read off THIS tree, the one the page ships, so a form grafted\n // from a layout, a component or a form entity is signed as it renders; and\n // after every stage that rewrites props, so nothing changes what the\n // signature covers.\n const withFormBindings = stampFormDatasetBindings(withLandmark, hostId)\n const denormalized = Aglyn.canvas.processNodesToDenormalized(\n withFormBindings as any,\n )\n /*\n * WHAT EACH PLACED IMAGE AND FILM IS NOW (AGL-2807, AGL-2833).\n *\n * An image's pixel pair and a film's length, shape and poster come from the\n * asset's DAM document as it is now, not as it was when it was picked. LAST,\n * on the tree the page ships, because an asset can arrive from any stage\n * above: a layout, a component, a form, a repeated row, a collection entry,\n * a binding.\n *\n * ONE read for all of them, issued once the tree is final rather than beside\n * the chrome reads. A read issued there could only cover the screen's own\n * placements, and nearly every layout places an image the screen does not\n * (a logo, a footer mark), so it would buy a second read on almost every\n * page. The late read costs its own round trip after the chrome reads, paid\n * each time a page is composed, which for a published page is its ISR\n * regeneration. A tree with no library asset, on a page whose social card\n * names none, issues none.\n *\n * THE SOCIAL CARD'S ASSETS JOIN THE SAME READ (AGL-2850), ahead of the\n * placements. A placement the cap leaves out keeps its pick-time pair, which\n * for an image is a reservation the decoded picture corrects. Nothing\n * corrects a card's pair, because a crawler lays the card out from it before\n * fetching the image, and a card names at most three documents.\n */\n const socialImages = options.socialImages\n const cardRefs = socialImages ? socialImageRefs(socialImages.images) : []\n const refs = mediaAssetRefs(denormalized)\n if (!refs.length && !cardRefs.length) return denormalized\n const facts = await getMediaAssetFacts({\n hostId,\n refs: [...cardRefs, ...refs],\n })\n if (socialImages) {\n const cardFacts = socialImageAssetFacts(socialImages.images, facts)\n if (cardFacts) socialImages.onFacts(cardFacts)\n }\n return applyMediaAssetFacts(denormalized, facts)\n}\n\n/**\n * Full published-render composition for one screen (extracted for AGL-87 so\n * the SSG path and the password-unlock API build identical trees): applies\n * a due publish schedule, loads the version, composes the shared layout\n * chrome, grafts reusable components, and denormalizes.\n */\nexport async function composeScreenNodes(options: {\n hostId: string\n screenId: string\n screen: Aglyn.AglynScreen\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * Compose a specific version instead of the published one (AGL-253):\n * experiment variants point at versions; schedules don't apply.\n */\n versionId?: string\n /** The host document, for `host.*` tokens (AGL-1022). */\n host?: Aglyn.HostTokenSource | null\n /**\n * The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of\n * the org and the host, resolved ONCE by the caller that holds both.\n *\n * A string rather than the two documents, because the value has to be\n * identical on the server render and the client re-render, and the surest\n * way to guarantee that is for only one place to decide it. Absent is UTC,\n * which is what every site rendered before this existed.\n */\n timeZone?: string\n /** The social card the head shares this page as (AGL-2850). */\n socialImages?: ComposeSocialImages\n}): Promise<Record<string, any> | null> {\n const { hostId, screenId, screen } = options\n\n const effectiveVersionId = options.versionId\n ? null\n : await applyDuePublishSchedule({\n hostId,\n collectionName: 'screens',\n docId: screenId,\n parent: screen,\n })\n const versionId = (options.versionId ??\n effectiveVersionId ??\n screen.versionId) as string\n\n /*\n * OVERLAP THE VERSION READ WITH THE CHROME BUNDLE (AGL-1428).\n *\n * `composeNodesWithChrome`'s reads are keyed by `hostId`/`layoutId`, both\n * of which are in hand here, so the version read no longer has to finish\n * before they start. Handing the promise over instead of the resolved\n * value is the whole change: it is `composeNodesWithChrome` that decides\n * when it actually needs the nodes.\n *\n * The `versionId`-less screen still exits BEFORE anything is issued, so a\n * screen that has never been published costs no reads. What remains is the\n * narrow case of a `versionId` that points at a missing or unreadable\n * version document — a data-integrity fault rather than a routing outcome\n * — and that one now pays for a chrome bundle it discards. That is the\n * deliberate trade: one wasted bundle on a broken screen, against the\n * version read hiding under the chrome reads on every render that works.\n */\n if (!versionId) return null\n\n const versionPromise = getScreenVersion({ hostId, screenId, versionId })\n\n /*\n * NEITHER DERIVED PROMISE MAY REJECT ON ITS OWN.\n *\n * `composed` is discarded on every failure path below, and a rejected\n * promise nobody awaited is an unhandled rejection — which in Node takes\n * the whole render process down instead of letting this 404. So the nodes\n * handed to the compose absorb the failure (an empty tree it will never be\n * asked for), and `composed` gets a rejection handler attached the moment\n * it exists rather than at the point we decide to drop it. The real error\n * still propagates, from the `await versionPromise` below, exactly where it\n * did when this function awaited the version directly.\n */\n const composed = composeNodesWithChrome({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n hostId,\n // Version-first (key-present wins, null = explicitly no layout), screen\n // fallback — resolved as a promise so only the layout-chain walk waits on\n // the version read; the rest of the chrome bundle keeps the AGL-1428\n // overlap. A failed version read falls back to the screen binding; the\n // whole compose is discarded on that path anyway.\n layoutId: versionPromise.then(\n (res) =>\n res.version && 'layoutId' in res.version\n ? ((res.version as Aglyn.AglynScreenVersion).layoutId as\n | string\n | null)\n : (screen.layoutId as string | undefined),\n () => screen.layoutId as string | undefined,\n ),\n // The screen's values for its layouts' properties (AGL-2893), beside the\n // binding on the same document; a failed read renders the defaults.\n layoutPropValues: versionPromise.then(\n (res) =>\n (res.version as Aglyn.AglynScreenVersion | undefined)?.layoutPropValues,\n () => undefined,\n ),\n screenNodes: versionPromise.then(\n (res) => (res.version?.nodes ?? {}) as any,\n () => ({}) as any,\n ),\n tokens: options.tokens,\n collection: options.collection,\n host: options.host,\n socialImages: options.socialImages,\n })\n void composed.catch(() => undefined)\n\n const versionRes = await versionPromise\n if (versionRes.error || !versionRes.version) return null\n\n return composed\n}\n\nexport default composeScreenNodes\n"],"names":["Aglyn","applyMediaAssetFacts","mediaAssetRefs","applyDuePublishSchedule","getComponents","getDatasets","getForms","getMediaAssetFacts","getPublishedCollectionSource","getPluginInstalls","getVariables","getFunctions","getWorkflows","getPublishedLayoutVersion","getScreenVersion","socialImageAssetFacts","socialImageRefs","stampFormDatasetBindings","scanCollectionBlocks","nodes","collection","slugs","Set","hasRelated","hasCategories","hasSearch","node","Object","values","componentId","COLLECTION_ENTRIES_COMPONENT_ID","slug","String","props","collectionSlug","trim","add","COLLECTION_CATEGORIES_COMPONENT_ID","COLLECTION_SEARCH_COMPONENT_ID","COLLECTION_RELATED_COMPONENT_ID","entry","prefetchCollectionSources","hostId","screenNodes","prefetched","entries","pending","catch","undefined","expandCollectionEntryBlocks","timeZone","size","sources","Promise","all","map","categories","page","entriesReachedBound","reachedBound","windowStart","fetched","expanded","expandCollectionEntries","withCategories","expandCollectionCategories","routeless","categorySlug","withSearch","expandCollectionSearch","expandCollectionRelated","composeNodesWithChrome","options","layoutId","walkLayoutChain","chain","seen","boundLayoutId","currentLayoutId","has","length","MAX_LAYOUT_CHAIN_DEPTH","layoutRes","push","version","parentId","layout","chromeBundle","screenDatasetKeys","repeatDatasetKeys","screenDatasetsPromise","keys","screenFormsPromise","placesFormEntity","prefetchedSources","layoutChain","componentsRes","bulk","rawVariables","functions","workflows","pluginInstalls","screenDatasets","layoutPropValues","composedNodes","composeLayoutChainWithProps","graftedComponents","composeReusableComponentNodes","definitions","forms","grafted","placedFormPlacement","variables","resolveComputedVariables","datasetKeys","unreadDatasetKeys","filter","key","includes","datasets","repeated","expandRepeatables","withEntries","withEntryMeta","expandCollectionEntryMeta","withEntryAuthor","expandCollectionEntryAuthor","bound","resolveNodesBindings","withHostTokens","resolveNodesHostTokens","host","withFunctions","attachFunctionDefinitions","attachPluginInstalls","finalNodes","resolveNamedTokens","tokens","withLandmark","stampDocumentLandmark","withFormBindings","denormalized","canvas","processNodesToDenormalized","socialImages","cardRefs","images","refs","facts","cardFacts","onFacts","composeScreenNodes","screenId","screen","effectiveVersionId","versionId","collectionName","docId","parent","versionPromise","composed","then","res","versionRes","error"],"mappings":"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"}
|
|
@@ -30,6 +30,11 @@ import { type SearchResultsNodesOptions } from './search-results-nodes';
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function composeSearchPage(options: {
|
|
32
32
|
hostId: string;
|
|
33
|
+
/**
|
|
34
|
+
* The zone this site's dates read in (AGL-3237); UTC when a site has not
|
|
35
|
+
* named one. Resolved once by the caller that holds the org and the host.
|
|
36
|
+
*/
|
|
37
|
+
timeZone?: string;
|
|
33
38
|
host: any;
|
|
34
39
|
results: SearchResultsNodesOptions;
|
|
35
40
|
}): Promise<{
|
|
@@ -36,12 +36,14 @@ import { buildSearchResultsNodes } from "./search-results-nodes.js";
|
|
|
36
36
|
hostId,
|
|
37
37
|
host
|
|
38
38
|
});
|
|
39
|
-
const nodes = await composeNodesWithChrome({
|
|
39
|
+
const nodes = await composeNodesWithChrome(_extends({}, options.timeZone ? {
|
|
40
|
+
timeZone: options.timeZone
|
|
41
|
+
} : {}, {
|
|
40
42
|
hostId,
|
|
41
43
|
layoutId,
|
|
42
44
|
screenNodes: buildSearchResultsNodes(results),
|
|
43
45
|
host: host
|
|
44
|
-
});
|
|
46
|
+
}));
|
|
45
47
|
return nodes ? _extends({
|
|
46
48
|
nodes
|
|
47
49
|
}, layoutId ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-search-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport { composeNodesWithChrome } from './compose-screen-nodes'\nimport {\n buildSearchResultsNodes,\n type SearchResultsNodesOptions,\n} from './search-results-nodes'\n\n/**\n * `/search` composed the way every other page of the site is (AGL-2513):\n * the built-in results body grafted into the site's shared layout, so the\n * page carries the same header, nav and footer as the pages it links to.\n *\n * Which layout is the host's to choose — see `resolveBuiltInPageLayoutId`.\n * With none resolvable the body composes alone, which is the page as it was\n * before this existed: themed, chrome-less, and still a working search page.\n *\n * Fail-open to `null`, and the route then renders the body with no chrome at\n * all. A search page is where a lost visitor goes; it must not be the page\n * that 500s because a layout was deleted.\n */\nexport async function composeSearchPage(options: {\n hostId: string\n host: any\n results: SearchResultsNodesOptions\n}): Promise<{ nodes: Record<string, any>; layoutId?: string } | null> {\n const { hostId, host, results } = options\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildSearchResultsNodes(results),\n host: host as Aglyn.HostTokenSource,\n })\n return nodes ? { nodes, ...(layoutId ? { layoutId } : {}) } : null\n } catch (error) {\n console.error('search page composition failed', error)\n return null\n }\n}\n\nexport default composeSearchPage\n"],"names":["resolveBuiltInPageLayoutId","composeNodesWithChrome","buildSearchResultsNodes","composeSearchPage","options","hostId","host","results","layoutId","nodes","screenNodes","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,0BAA0B,QAAQ,4BAAwB;AACnE,SAASC,sBAAsB,QAAQ,4BAAwB;AAC/D,SACEC,uBAAuB,QAElB,4BAAwB;AAE/B;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,kBAAkBC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-search-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport { composeNodesWithChrome } from './compose-screen-nodes'\nimport {\n buildSearchResultsNodes,\n type SearchResultsNodesOptions,\n} from './search-results-nodes'\n\n/**\n * `/search` composed the way every other page of the site is (AGL-2513):\n * the built-in results body grafted into the site's shared layout, so the\n * page carries the same header, nav and footer as the pages it links to.\n *\n * Which layout is the host's to choose — see `resolveBuiltInPageLayoutId`.\n * With none resolvable the body composes alone, which is the page as it was\n * before this existed: themed, chrome-less, and still a working search page.\n *\n * Fail-open to `null`, and the route then renders the body with no chrome at\n * all. A search page is where a lost visitor goes; it must not be the page\n * that 500s because a layout was deleted.\n */\nexport async function composeSearchPage(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237); UTC when a site has not\n * named one. Resolved once by the caller that holds the org and the host.\n */\n timeZone?: string\n host: any\n results: SearchResultsNodesOptions\n}): Promise<{ nodes: Record<string, any>; layoutId?: string } | null> {\n const { hostId, host, results } = options\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const nodes = await composeNodesWithChrome({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n hostId,\n layoutId,\n screenNodes: buildSearchResultsNodes(results),\n host: host as Aglyn.HostTokenSource,\n })\n return nodes ? { nodes, ...(layoutId ? { layoutId } : {}) } : null\n } catch (error) {\n console.error('search page composition failed', error)\n return null\n }\n}\n\nexport default composeSearchPage\n"],"names":["resolveBuiltInPageLayoutId","composeNodesWithChrome","buildSearchResultsNodes","composeSearchPage","options","hostId","host","results","layoutId","nodes","timeZone","screenNodes","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,0BAA0B,QAAQ,4BAAwB;AACnE,SAASC,sBAAsB,QAAQ,4BAAwB;AAC/D,SACEC,uBAAuB,QAElB,4BAAwB;AAE/B;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,kBAAkBC,OASvC;IACC,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAE,GAAGH;IAClC,IAAI;QACF,MAAMI,WAAW,MAAMR,2BAA2B;YAAEK;YAAQC;QAAK;QACjE,MAAMG,QAAQ,MAAMR,uBAAuB,aACrCG,QAAQM,QAAQ,GAAG;YAAEA,UAAUN,QAAQM,QAAQ;QAAC,IAAI,CAAC;YACzDL;YACAG;YACAG,aAAaT,wBAAwBK;YACrCD,MAAMA;;QAER,OAAOG,QAAQ;YAAEA;WAAWD,WAAW;YAAEA;QAAS,IAAI,CAAC,KAAO;IAChE,EAAE,OAAOI,OAAO;QACdC,QAAQD,KAAK,CAAC,kCAAkCA;QAChD,OAAO;IACT;AACF;AAEA,eAAeT,kBAAiB"}
|