@aglyn/tenant-runtime 1.0.0-beta.156 → 1.0.0-beta.158

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-runtime",
3
- "version": "1.0.0-beta.156",
3
+ "version": "1.0.0-beta.158",
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.156",
29
- "@aglyn/tenant-data-admin": "1.0.0-beta.156",
28
+ "@aglyn/aglyn": "1.0.0-beta.158",
29
+ "@aglyn/tenant-data-admin": "1.0.0-beta.158",
30
30
  "@swc/helpers": "0.5.23"
31
31
  },
32
32
  "peerDependencies": {
@@ -139,7 +139,9 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
139
139
  if (!templateRes.screen) return null;
140
140
  const { cardImages, tokens, collection } = authorComposeContext(content);
141
141
  const card = collectSocialImageFacts(cardImages);
142
- const nodes = await composeScreenNodes({
142
+ const nodes = await composeScreenNodes(_extends({}, options.timeZone ? {
143
+ timeZone: options.timeZone
144
+ } : {}, {
143
145
  hostId,
144
146
  screenId,
145
147
  screen: templateRes.screen,
@@ -149,7 +151,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
149
151
  // The designated screen's host variables, and its layout's, fill in
150
152
  // from this site, as the built-in author page's already do (AGL-2883).
151
153
  host: host
152
- });
154
+ }));
153
155
  if (!nodes) return null;
154
156
  /*
155
157
  The screen document travels UNCHANGED, and deliberately carries no
@@ -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 /**\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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"}
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 // The site's zone (AGL-3237). This is the TEMPLATE path, which\n // composes through `composeScreenNodes` rather than\n // `composeNodesWithChrome` — and that difference is why the first two\n // passes at this fix reached every page except the one the bug was\n // reported on.\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\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","timeZone","socialImages","expandContentAuthorProfile","collected","error","console","composeAuthorFallbackPage","layoutId","screenNodes","hasEntries","length","perPage"],"mappings":"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"}
@@ -115,7 +115,9 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
115
115
  (_templateRes_screen_seo1 = templateRes.screen.seo) == null ? void 0 : _templateRes_screen_seo1.image,
116
116
  (_options_host = options.host) == null ? void 0 : (_options_host_seo = _options_host.seo) == null ? void 0 : _options_host_seo.image
117
117
  ]);
118
- const nodes = await composeScreenNodes({
118
+ const nodes = await composeScreenNodes(_extends({}, options.timeZone ? {
119
+ timeZone: options.timeZone
120
+ } : {}, {
119
121
  hostId,
120
122
  screenId,
121
123
  screen: templateRes.screen,
@@ -150,7 +152,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
150
152
  } : {}, ((_content_pagination1 = content.pagination) == null ? void 0 : _content_pagination1.windowStart) ? {
151
153
  windowStart: content.pagination.windowStart
152
154
  } : {})
153
- });
155
+ }));
154
156
  if (!nodes) return null;
155
157
  const screenSeo = (_templateRes_screen_seo = templateRes.screen.seo) != null ? _templateRes_screen_seo : {};
156
158
  const seo = entry ? _extends({}, screenSeo, {
@@ -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 /**\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 options.timeZone,\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","timeZone","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 options.timeZone,\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 // The site's zone (AGL-3237). This is the TEMPLATE path, which\n // composes through `composeScreenNodes` rather than\n // `composeNodesWithChrome` — and that difference is why the first two\n // passes at this fix reached every page except the one the bug was\n // reported on.\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\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","timeZone","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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"}