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

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.155",
3
+ "version": "1.0.0-beta.156",
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.155",
29
- "@aglyn/tenant-data-admin": "1.0.0-beta.155",
28
+ "@aglyn/aglyn": "1.0.0-beta.156",
29
+ "@aglyn/tenant-data-admin": "1.0.0-beta.156",
30
30
  "@swc/helpers": "0.5.23"
31
31
  },
32
32
  "peerDependencies": {
@@ -107,7 +107,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
107
107
  });
108
108
  if (!templateRes.screen) return null;
109
109
  const entry = content.entry;
110
- const tokens = entry ? _extends({}, collectionTokens(collection), Aglyn.collectionEntryTokens(entry, collection.slug, collection.categories)) : collectionTokens(collection, content.category, content.pagination);
110
+ const tokens = entry ? _extends({}, collectionTokens(collection), Aglyn.collectionEntryTokens(entry, collection.slug, collection.categories, options.timeZone)) : collectionTokens(collection, content.category, content.pagination);
111
111
  // The head's card on this page, in its order: the entry's cover, the
112
112
  // template's own image, then the site default (AGL-2850).
113
113
  const card = collectSocialImageFacts([
@@ -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 ),\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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"}
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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"}
@@ -188,7 +188,7 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
188
188
  taxonomy — "browse the blog by category", which is a sensible thing
189
189
  to put beside an archive.
190
190
  */ (collection == null ? void 0 : collection.routeless) ? undefined : collection == null ? void 0 : collection.slug, collection == null ? void 0 : collection.categorySlug) : expanded;
191
- const withSearch = hasSearch ? Aglyn.expandCollectionSearch(withCategories, sources, collection == null ? void 0 : collection.slug) : withCategories;
191
+ const withSearch = hasSearch ? Aglyn.expandCollectionSearch(withCategories, sources, collection == null ? void 0 : collection.slug, timeZone) : withCategories;
192
192
  if (!hasRelated || !(collection == null ? void 0 : collection.entry)) return withSearch;
193
193
  return Aglyn.expandCollectionRelated(withSearch, sources[collection.slug], collection.entry, timeZone);
194
194
  }
@@ -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 /** 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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"}
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(\n withCategories,\n sources,\n collection?.slug,\n timeZone,\n )\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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,uEAAuE;AACvE,sDAAsD;AACtD,SACEC,oBAAoB,EACpBC,cAAc,QACT,2CAA0C;AACjD,OAAOC,6BAA6B,8BAA0B;AAC9D,OAAOC,mBAAmB,sBAAkB;AAC5C,OAAOC,iBAAiB,oBAAgB;AACxC,OAAOC,cAAc,iBAAa;AAClC,OAAOC,wBAAwB,6BAAyB;AACxD,SACEC,4BAA4B,QAEvB,8BAA0B;AACjC,OAAOC,uBAAuB,2BAAuB;AACrD,OAAOC,gBAAgBC,YAAY,EAAEC,YAAY,QAAQ,qBAAiB;AAC1E,OAAOC,+BAA+B,0BAAsB;AAC5D,OAAOC,sBAAsB,0BAAsB;AACnD,SAEEC,qBAAqB,EACrBC,eAAe,QACV,0BAAsB;AAC7B,SAASC,wBAAwB,QAAQ,mCAA+B;AAwExE;;;;;;;;;CASC,GACD,SAASC,qBACPC,KAA0B,EAC1BC,UAAqC;IAErC,MAAMC,QAAQ,IAAIC;IAClB,IAAIC,aAAa;IACjB,IAAIC,gBAAgB;IACpB,IAAIC,YAAY;IAChB,KAAK,MAAMC,QAAQC,OAAOC,MAAM,CAACT,OAAQ;QACvC,IAAIO,CAAAA,wBAAAA,KAAMG,WAAW,MAAK7B,MAAM8B,+BAA+B,EAAE;;gBAEtDJ;YADT,MAAMK,OACJC,eAAON,yBAAAA,cAAAA,KAAMO,KAAK,qBAAXP,YAAaQ,cAAc,mBAAI,IAAIC,IAAI,OAAMf,8BAAAA,WAAYW,IAAI;YACtE,IAAIA,MAAMV,MAAMe,GAAG,CAACL;QACtB;QACA,wEAAwE;QACxE,yDAAyD;QACzD,IAAIL,CAAAA,wBAAAA,KAAMG,WAAW,MAAK7B,MAAMqC,kCAAkC,EAAE;;gBAEzDX;YADT,MAAMK,OACJC,gBAAON,yBAAAA,eAAAA,KAAMO,KAAK,qBAAXP,aAAaQ,cAAc,oBAAI,IAAIC,IAAI,OAAMf,8BAAAA,WAAYW,IAAI;YACtE,IAAIA,MAAM;gBACRP,gBAAgB;gBAChBH,MAAMe,GAAG,CAACL;YACZ;QACF;QACA,yEAAyE;QACzE,uEAAuE;QACvE,mDAAmD;QACnD,IAAIL,CAAAA,wBAAAA,KAAMG,WAAW,MAAK7B,MAAMsC,8BAA8B,EAAE;;gBAErDZ;YADT,MAAMK,OACJC,gBAAON,yBAAAA,eAAAA,KAAMO,KAAK,qBAAXP,aAAaQ,cAAc,oBAAI,IAAIC,IAAI,OAAMf,8BAAAA,WAAYW,IAAI;YACtE,IAAIA,MAAM;gBACRN,YAAY;gBACZJ,MAAMe,GAAG,CAACL;YACZ;QACF;QACA,uEAAuE;QACvE,8DAA8D;QAC9D,IACEL,CAAAA,wBAAAA,KAAMG,WAAW,MAAK7B,MAAMuC,+BAA+B,KAC3DnB,8BAAAA,WAAYW,IAAI,KAChBX,WAAWoB,KAAK,EAChB;YACAjB,aAAa;YACbF,MAAMe,GAAG,CAAChB,WAAWW,IAAI;QAC3B;IACF;IACA,OAAO;QAAEV;QAAOE;QAAYC;QAAeC;IAAU;AACvD;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAASgB,0BACPC,MAAc,EACdC,WAAgC,EAChCvB,UAAqC;IAErC,MAAMwB,aAGF,CAAC;IACL,KAAK,MAAMb,QAAQb,qBAAqByB,aAAavB,YAAYC,KAAK,CAAE;QACtE,IAAIU,UAASX,8BAAAA,WAAYW,IAAI,KAAIX,WAAWyB,OAAO,EAAE;QACrD,MAAMC,UAAUtC,6BAA6B;YAC3CkC;YACAR,gBAAgBH;QAClB;QACA,uEAAuE;QACvE,0EAA0E;QAC1E,2EAA2E;QAC3E,wEAAwE;QACxE,KAAKe,QAAQC,KAAK,CAAC,IAAMC;QACzBJ,UAAU,CAACb,KAAK,GAAGe;IACrB;IACA,OAAOF;AACT;AAEA;;;;;CAKC,GACD,eAAeK,4BACbP,MAAc,EACdvB,KAA0B,EAC1BC,UAAqC,EACrCwB,UAA+D,EAC/D,mEAAmE,GACnEM,QAAiB;IAEjB,MAAM,EAAE7B,KAAK,EAAEE,UAAU,EAAEC,aAAa,EAAEC,SAAS,EAAE,GAAGP,qBACtDC,OACAC;IAEF,IAAI,CAACC,MAAM8B,IAAI,EAAE,OAAOhC;IACxB,MAAMiC,UAAyD,CAAC;IAChE,MAAMC,QAAQC,GAAG,CACf;WAAIjC;KAAM,CAACkC,GAAG,CAAC,OAAOxB;;QACpB,4DAA4D;QAC5D,gEAAgE;QAChE,IAAIA,UAASX,8BAAAA,WAAYW,IAAI,KAAIX,WAAWyB,OAAO,EAAE;YACnDO,OAAO,CAACrB,KAAK,GAAG;gBACdA;gBACAc,SAASzB,WAAWyB,OAAO;gBAC3BW,YAAYpC,WAAWoC,UAAU;eAG7BpC,WAAWqC,IAAI,GAAG;gBAAEA,MAAMrC,WAAWqC,IAAI;YAAC,IAAI,CAAC,GAG/CrC,WAAWsC,mBAAmB,GAAG;gBAAEC,cAAc;YAAK,IAAI,CAAC,GAG3DvC,WAAWwC,WAAW,GACtB;gBAAEA,aAAaxC,WAAWwC,WAAW;YAAC,IACtC,CAAC;YAEP;QACF;QACA,qEAAqE;QACrE,qEAAqE;QACrE,MAAMC,UAAU,eAAOjB,8BAAAA,UAAY,CAACb,KAAK,mBACvCvB,6BAA6B;YAAEkC;YAAQR,gBAAgBH;QAAK;QAC9DqB,OAAO,CAACrB,KAAK,GAAG;YACdA;YACAc,SAASgB,QAAQhB,OAAO;YACxBW,YACEzB,UAASX,8BAAAA,WAAYW,IAAI,KAAIX,WAAWoC,UAAU,GAC9CpC,WAAWoC,UAAU,GACrBK,QAAQL,UAAU;WACpBK,QAAQF,YAAY,GAAG;YAAEA,cAAc;QAAK,IAAI,CAAC;IAEzD;IAEF,MAAMG,WAAW9D,MAAM+D,uBAAuB,CAC5C5C,OACAiC,SACAhC,8BAAAA,WAAYW,IAAI,EAChBmB;IAEF,MAAMc,iBAAiBxC,gBACnBxB,MAAMiE,0BAA0B,CAC9BH,UACAV,SACA;;;;;;;;;;;;;;;;;QAiBA,GACAhC,CAAAA,8BAAAA,WAAY8C,SAAS,IAAGlB,YAAY5B,8BAAAA,WAAYW,IAAI,EACpDX,8BAAAA,WAAY+C,YAAY,IAE1BL;IACJ,MAAMM,aAAa3C,YACfzB,MAAMqE,sBAAsB,CAC1BL,gBACAZ,SACAhC,8BAAAA,WAAYW,IAAI,EAChBmB,YAEFc;IACJ,IAAI,CAACzC,cAAc,EAACH,8BAAAA,WAAYoB,KAAK,GAAE,OAAO4B;IAC9C,OAAOpE,MAAMsE,uBAAuB,CAClCF,YACAhB,OAAO,CAAChC,WAAWW,IAAI,CAAC,EACxBX,WAAWoB,KAAK,EAChBU;AAEJ;AAEA;;;;;;;;;CASC,GACD,OAAO,eAAeqB,uBAAuBC,OAiE5C;;QAyKI,OA0DDA,qBACAA,sBAQAA;IA3OF,MAAM,EAAE9B,MAAM,EAAE+B,QAAQ,EAAE,GAAGD;IAE7B;;;;;;;;;;;;;GAaC,GACD,MAAME,kBAAkB;QACtB,MAAMC,QAAkC,EAAE;QAC1C,MAAMC,OAAO,IAAItD;QACjB,MAAMuD,gBAAgB,MAAMJ;QAC5B,IAAIK,kBAAkBD,gBAAgB7C,OAAO6C,iBAAiB7B;QAC9D,MACE8B,mBACA,CAACF,KAAKG,GAAG,CAACD,oBACVH,MAAMK,MAAM,GAAGhF,MAAMiF,sBAAsB,CAC3C;gBAWSC,oBACCA,qBAGQA;YAdlBN,KAAKxC,GAAG,CAAC0C;YACT,MAAMI,YAAY,MAAMrE,0BAA0B;gBAChD6B;gBACA+B,UAAUK;YACZ;YACA,sEAAsE;YACtE,sEAAsE;YACtE,2DAA2D;YAC3DH,MAAMQ,IAAI,CAAC;gBACTV,UAAUK;gBACV3D,KAAK,EAAE+D,8BAAAA,qBAAAA,UAAWE,OAAO,qBAAlBF,mBAAoB/D,KAAK;gBAChCc,KAAK,EAAGiD,8BAAAA,sBAAAA,UAAWE,OAAO,qBAAnB,AAACF,oBACJjD,KAAK;YACX;YACA,MAAMoD,WAAYH,8BAAAA,oBAAAA,UAAWI,MAAM,qBAAlB,AAACJ,kBAA2BT,QAAQ;YACrDK,kBAAkBO,WAAWrD,OAAOqD,YAAYrC;QAClD;QACA,OAAO2B;IACT;IAEA;;;;;;;;;;;;;;;;;;;;GAoBC,GACD;;;;;;;;;;;;;GAaC,GACD;;;;;;;;;;;;;;;GAeC,GACD,MAAMY,eAAelC,QAAQC,GAAG,CAAC;QAC/BoB;QACAtE,cAAc;YAAEsC;QAAO;QACvB,8DAA8D;QAC9D,sEAAsE;QACtE,gCAAgC;QAChCW,QAAQC,GAAG,CAAC;YACV5C,aAAa;gBAAEgC;YAAO;YACtB/B,aAAa;gBAAE+B;YAAO;YACtB9B,aAAa;gBAAE8B;YAAO;YACtBjC,kBAAkB;gBAAEiC;YAAO;SAC5B;KACF;IACD,MAAMC,cAAc,MAAM6B,QAAQ7B,WAAW;IAC7C,MAAM6C,oBAAoBxF,MAAMyF,iBAAiB,CAAC9C;IAClD,MAAM+C,wBAAwBF,kBAAkBR,MAAM,GAClD3E,YAAY;QAAEqC;QAAQiD,MAAMH;IAAkB,KAC9CxC;IACJ,yEAAyE;IACzE,6EAA6E;IAC7E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,yDAAyD;IACzD,MAAM4C,qBAAqB5F,MAAM6F,gBAAgB,CAAClD,eAC9CrC,SAAS;QAAEoC;IAAO,KAClBM;IACJ,wEAAwE;IACxE,sEAAsE;IACtE,MAAM8C,oBAAoBrD,0BACxBC,QACAC,aACA6B,QAAQpD,UAAU;IAEpB,MAAM,CAAC2E,aAAaC,eAAeC,KAAK,GAAG,MAAMV;IACjD,MAAM,CAACW,cAAcC,WAAWC,WAAWC,eAAe,GAAGJ;IAC7D,MAAMK,iBAAiB,MAAMZ;IAC7B,sEAAsE;IACtE,8CAA8C;IAC9C,MAAMa,mBAAmB,MAAM/B,QAAQ+B,gBAAgB;IAEvD,MAAMC,gBAAgBxG,MAAMyG,2BAA2B,CACrDV,aACApD,aACA4D;IAEF,MAAMG,oBAAoB1G,MAAM2G,6BAA6B,CAC3DH,eACAR,cAAcY,WAAW;IAE3B;;;;;;;;;;;;;;;;;;;GAmBC,GACD,MAAMC,iBACH,QAAA,MAAMjB,uCAAP,AAAC,MAA2BiB,KAAK,mBAChC7G,MAAM6F,gBAAgB,CAACa,qBACpB,AAAC,CAAA,MAAMpG,SAAS;QAAEoC;IAAO,EAAC,EAAGmE,KAAK,GAClC7D;IACN,MAAM8D,UAAUD,QACZ7G,MAAM2G,6BAA6B,CACjCD,mBACAV,cAAcY,WAAW,EACzB;QAAC5G,MAAM+G,mBAAmB,CAACF;KAAc,IAE3CH;IACJ,wEAAwE;IACxE,0DAA0D;IAC1D,MAAMM,YAAYhH,MAAMiH,wBAAwB,CAC9Cf,cACAC,WACAC;IAEF,mEAAmE;IACnE,sEAAsE;IACtE,+BAA+B;IAC/B,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,oEAAoE;IACpE,4EAA4E;IAC5E,+DAA+D;IAC/D,MAAMc,cAAclH,MAAMyF,iBAAiB,CAACqB;IAC5C,MAAMK,oBAAoBD,YAAYE,MAAM,CAC1C,CAACC,MAAQ,CAAC7B,kBAAkB8B,QAAQ,CAACD;IAEvC,MAAME,WAAWJ,kBAAkBnC,MAAM,GACrC,aACKsB,gBACC,MAAMjG,YAAY;QAAEqC;QAAQiD,MAAMwB;IAAkB,MAE1Db;IACJ,MAAMkB,WAAWxH,MAAMyH,iBAAiB,CAACX,SAAgBS;IACzD,oEAAoE;IACpE,wEAAwE;IACxE,uDAAuD;IACvD,MAAMG,cAAc,MAAMzE,4BACxBP,QACA8E,UACAhD,QAAQpD,UAAU,EAClB0E,mBACAtB,QAAQtB,QAAQ;IAElB,0EAA0E;IAC1E,sEAAsE;IACtE,4EAA4E;IAC5E,2EAA2E;IAC3E,4EAA4E;IAC5E,mBAAmB;IACnB,MAAMyE,gBAAgB3H,MAAM4H,yBAAyB,CACnDF,cACAlD,sBAAAA,QAAQpD,UAAU,qBAAlBoD,oBAAoBhC,KAAK,GACzBgC,uBAAAA,QAAQpD,UAAU,qBAAlBoD,qBAAoBhB,UAAU,EAC9BgB,QAAQtB,QAAQ;IAElB,2EAA2E;IAC3E,qEAAqE;IACrE,sEAAsE;IACtE,MAAM2E,kBAAkB7H,MAAM8H,2BAA2B,CACvDH,gBACAnD,uBAAAA,QAAQpD,UAAU,qBAAlBoD,qBAAoBhC,KAAK;IAE3B,MAAMuF,QAAQ/H,MAAMgI,oBAAoB,CACtCH,iBACAb,WACAb;IAEF,uEAAuE;IACvE,wEAAwE;IACxE,qEAAqE;IACrE,oEAAoE;IACpE,MAAM8B,iBAAiBjI,MAAMkI,sBAAsB,CACjDH,OACAvD,QAAQ2D,IAAI;IAEd,0EAA0E;IAC1E,mEAAmE;IACnE,cAAc;IACd,MAAMC,gBAAgBpI,MAAMqI,yBAAyB,CACnDJ,gBACA9B,WACAa;IAEF,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM7F,QAAQnB,MAAMsI,oBAAoB,CAACF,eAAe/B;IACxD,wEAAwE;IACxE,MAAMkC,aAAavI,MAAMwI,kBAAkB,CAACrH,OAAcqD,QAAQiE,MAAM;IACxE,4EAA4E;IAC5E,2EAA2E;IAC3E,sDAAsD;IACtD,MAAMC,eAAe1I,MAAM2I,qBAAqB,CAACJ;IACjD,uEAAuE;IACvE,4EAA4E;IAC5E,2EAA2E;IAC3E,qEAAqE;IACrE,oBAAoB;IACpB,MAAMK,mBAAmB3H,yBAAyByH,cAAchG;IAChE,MAAMmG,eAAe7I,MAAM8I,MAAM,CAACC,0BAA0B,CAC1DH;IAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBC,GACD,MAAMI,eAAexE,QAAQwE,YAAY;IACzC,MAAMC,WAAWD,eAAehI,gBAAgBgI,aAAaE,MAAM,IAAI,EAAE;IACzE,MAAMC,OAAOjJ,eAAe2I;IAC5B,IAAI,CAACM,KAAKnE,MAAM,IAAI,CAACiE,SAASjE,MAAM,EAAE,OAAO6D;IAC7C,MAAMO,QAAQ,MAAM7I,mBAAmB;QACrCmC;QACAyG,MAAM;eAAIF;eAAaE;SAAK;IAC9B;IACA,IAAIH,cAAc;QAChB,MAAMK,YAAYtI,sBAAsBiI,aAAaE,MAAM,EAAEE;QAC7D,IAAIC,WAAWL,aAAaM,OAAO,CAACD;IACtC;IACA,OAAOpJ,qBAAqB4I,cAAcO;AAC5C;AAEA;;;;;CAKC,GACD,OAAO,eAAeG,mBAAmB/E,OA2BxC;QAWoBA,MAAAA;IAVnB,MAAM,EAAE9B,MAAM,EAAE8G,QAAQ,EAAEC,MAAM,EAAE,GAAGjF;IAErC,MAAMkF,qBAAqBlF,QAAQmF,SAAS,GACxC,OACA,MAAMxJ,wBAAwB;QAC5BuC;QACAkH,gBAAgB;QAChBC,OAAOL;QACPM,QAAQL;IACV;IACJ,MAAME,aAAanF,QAAAA,qBAAAA,QAAQmF,SAAS,YAAjBnF,qBACjBkF,8BADiBlF,OAEjBiF,OAAOE,SAAS;IAElB;;;;;;;;;;;;;;;;GAgBC,GACD,IAAI,CAACA,WAAW,OAAO;IAEvB,MAAMI,iBAAiBjJ,iBAAiB;QAAE4B;QAAQ8G;QAAUG;IAAU;IAEtE;;;;;;;;;;;GAWC,GACD,MAAMK,WAAWzF,uBAAuB,aAClCC,QAAQtB,QAAQ,GAAG;QAAEA,UAAUsB,QAAQtB,QAAQ;IAAC,IAAI,CAAC;QACzDR;QACA,wEAAwE;QACxE,0EAA0E;QAC1E,qEAAqE;QACrE,uEAAuE;QACvE,kDAAkD;QAClD+B,UAAUsF,eAAeE,IAAI,CAC3B,CAACC,MACCA,IAAI9E,OAAO,IAAI,cAAc8E,IAAI9E,OAAO,GACnC,AAAC8E,IAAI9E,OAAO,CAA8BX,QAAQ,GAGlDgF,OAAOhF,QAAQ,EACtB,IAAMgF,OAAOhF,QAAQ;QAEvB,yEAAyE;QACzE,oEAAoE;QACpE8B,kBAAkBwD,eAAeE,IAAI,CACnC,CAACC;gBACEA;oBAAAA,eAAAA,IAAI9E,OAAO,qBAAZ,AAAC8E,aAAsD3D,gBAAgB;WACzE,IAAMvD;QAERL,aAAaoH,eAAeE,IAAI,CAC9B,CAACC;;gBAASA;4BAAAA,eAAAA,IAAI9E,OAAO,qBAAX8E,aAAa/I,KAAK,mBAAI,CAAC;WACjC,IAAO,CAAA,CAAC,CAAA;QAEVsH,QAAQjE,QAAQiE,MAAM;QACtBrH,YAAYoD,QAAQpD,UAAU;QAC9B+G,MAAM3D,QAAQ2D,IAAI;QAClBa,cAAcxE,QAAQwE,YAAY;;IAEpC,KAAKgB,SAASjH,KAAK,CAAC,IAAMC;IAE1B,MAAMmH,aAAa,MAAMJ;IACzB,IAAII,WAAWC,KAAK,IAAI,CAACD,WAAW/E,OAAO,EAAE,OAAO;IAEpD,OAAO4E;AACT;AAEA,eAAeT,mBAAkB"}
@@ -141,6 +141,17 @@ export declare function scheduledPublishingPermission(hostId: string): Promise<S
141
141
  */
142
142
  export declare function isLive(value: FirebaseFirestore.DocumentData, permission: SchedulePermission): boolean;
143
143
  export interface CollectionContent {
144
+ /**
145
+ * The zone this site's dates read in (AGL-3237), carried on the CONTENT so
146
+ * it reaches the client.
147
+ *
148
+ * `collection-fallback.tsx` renders through `next/dynamic` from
149
+ * `catch-all-client`, so it formats dates in the browser as well as on the
150
+ * server. A zone it read from its own runtime would be the visitor's, which
151
+ * is precisely the hydration mismatch AGL-1926 fixed — so the server
152
+ * decides it once and it travels here, in the props both renders read.
153
+ */
154
+ timeZone?: string;
144
155
  collection: {
145
156
  $id: string;
146
157
  displayName: string;
@@ -373,6 +384,12 @@ export declare function getCollectionContent(options: {
373
384
  after?: string;
374
385
  /** Continue BEFORE this entry's document id — the newer direction. */
375
386
  before?: string;
387
+ /**
388
+ * The site's zone (AGL-3237). Stamped onto the returned content so every
389
+ * reader downstream — including the client fallback renderer — formats from
390
+ * the same string rather than from its own runtime.
391
+ */
392
+ timeZone?: string;
376
393
  /**
377
394
  * Category segment of `/{collection}/category/{slug}` (AGL-1321). Filters
378
395
  * the listing before pagination is computed, so page counts and the page
@@ -790,14 +790,16 @@ async function readPublishedCollectionSource(options) {
790
790
  // makes the grant addressable at all, and the list is the shared cached
791
791
  // read this must stay out of.
792
792
  const preview = Boolean(options.previewUnpublishedEntry) && Boolean(entrySlug);
793
- const data = {
793
+ const data = _extends({}, options.timeZone ? {
794
+ timeZone: options.timeZone
795
+ } : {}, {
794
796
  collection: null,
795
797
  entries: [],
796
798
  entry: null,
797
799
  pagination: null,
798
800
  category: null,
799
801
  error: null
800
- };
802
+ });
801
803
  try {
802
804
  var _entryQuery_docs_find;
803
805
  // A LIST route is served entirely from the CACHED source, which every
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-collection-content.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 {\n AUTHORS_MAX_PER_HOST,\n type CollectionCategory,\n collectionCategorySlug,\n checkEntitlement,\n COLLECTION_SOURCE_MAX,\n collectionTotalPages,\n type ContentAuthorRecord,\n entryMatchesCategoryRoute,\n hostCollectionKind,\n normalizeContentAuthor,\n normalizeContentSchemaType,\n resolveCollectionCategoryBySlug,\n resolveEntryAuthor,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * ONE cached read per collection, shared by every surface that lists it\n * (AGL-1302).\n *\n * It began as the compose-time source only: a Collection entries block in a\n * shared layout re-read up to ~100 entry docs on EVERY page of the site. The\n * routed listing was left out on the argument that the page's own ISR entry\n * amortized it — which is true of ONE address and false of a collection,\n * because a collection is not one address. `/blog`, `/blog/page/2…10`, a\n * `/blog/category/{slug}` per category and `/blog/rss.xml` are all the same\n * data, each paying its own `1 + entries + authors` per window, beside a\n * cache already holding exactly that.\n *\n * The other half of that argument was real and is answered rather than\n * dropped: `flipDueEntry` is a write, and nothing else publishes a content\n * entry, so a cache that stored a collection with a schedule still pending\n * would suppress the render that publishes it. `getPublishedCollectionSource`\n * therefore declines to STORE exactly those collections — see its `store`\n * predicate — which leaves scheduled publishing on the render window it has\n * always been on, and puts everything else on this TTL.\n */\nconst COLLECTION_SOURCE_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * Resolve a public content-collection slug (AGL-954). Commerce's product\n * collections share `hosts/{hostId}/collections`, and a slug is only unique\n * within a kind — a bare `limit(1)` handed the URL to whichever doc Firestore\n * returned first, so a catalog collection could shadow a blog. Reads a small\n * window instead and takes the first content-kind match.\n */\nasync function findContentCollection(\n hostId: string,\n collectionSlug: string,\n): Promise<FirebaseFirestore.QueryDocumentSnapshot | undefined> {\n const matches = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .where('slug', '==', collectionSlug)\n .limit(5)\n .get()\n return matches.docs.find(\n (docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'content',\n )\n}\n\nexport interface CollectionEntrySummary {\n $id: string\n title: string\n slug: string\n excerpt?: string\n /**\n * The byline TEXT (AGL-686). Either the entry's own legacy free-typed\n * string or — since AGL-2486 — the name of the author record `authorId`\n * points at, resolved here so every downstream reader (the Entry Meta\n * block, `{{entry.author}}`, the RSS feed) keeps asking one field.\n */\n authorName?: string\n /** Reference into `hosts/{hostId}/authors` (AGL-2486). */\n authorId?: string\n /**\n * The resolved author RECORD (AGL-2486) — what `Article.author` is built\n * from. Null when the entry names no author, in which case the page falls\n * back to the site's publisher entity exactly as it always has.\n */\n author?: ContentAuthorRecord | null\n body?: string\n coverImage?: string\n /** `og:image:alt` for the cover (AGL-2417); travels WITH `coverImage`. */\n coverImageAlt?: string\n /**\n * The featured video (AGL-2956): a media reference or a URL, a Wistia link\n * included, in the shape `coverImage` is. See\n * `CollectionEntryRecord.coverVideo`.\n */\n coverVideo?: string\n /** Search-result title override (AGL-582); falls back to `title`. */\n seoTitle?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /**\n * Stable reference into the collection's `categories` taxonomy\n * (AGL-582); resolved to a display name at render.\n */\n categoryId?: string\n /** Legacy free-typed bucket (AGL-582); read-only fallback. */\n category?: string\n /** Free-form labels (AGL-582). */\n tags?: string[]\n publishedAt?: { seconds: number } | null\n /**\n * Last edited, which is what `Article.dateModified` publishes (AGL-2534).\n *\n * Distinct from {@link publishedAt} on purpose: re-dating a post is not\n * editing it, so the console writes `publishedAt` alone when an author\n * backdates and this stays put. Google reads `dateModified` for freshness.\n */\n updatedAt?: { seconds: number } | null\n /**\n * The collection this entry came out of (AGL-2518), stamped only by a\n * reader that MIXES collections — the author page. Unset on every routed\n * listing, where the route already answers the question. See\n * `CollectionEntryRecord.collectionSlug`.\n */\n collectionSlug?: string\n /** The display name of {@link collectionSlug}. */\n collectionName?: string\n}\n\n/** Entry-doc fields shared by the list and single-entry mappers (AGL-582). */\nfunction mapEntryFields(\n value: FirebaseFirestore.DocumentData,\n): Pick<\n CollectionEntrySummary,\n | 'excerpt'\n | 'coverImage'\n | 'coverImageAlt'\n | 'coverVideo'\n | 'seoTitle'\n | 'seoDescription'\n | 'authorName'\n | 'authorId'\n | 'categoryId'\n | 'category'\n | 'tags'\n | 'updatedAt'\n> {\n return {\n excerpt: value['excerpt'] ?? '',\n // The byline was DECLARED on `CollectionEntrySummary` (AGL-686) and\n // mapped by nobody, so `entry.authorName` was `undefined` on every entry\n // this loader returned — which is every routed entry page and every\n // Collection entries block. The console collected the field, the rules\n // stored it, the JSON-LD builder read it and the Entry Meta block printed\n // it, and all three saw nothing, because the one hop between Firestore\n // and them dropped it (AGL-2486). Written but never read, in the\n // direction that leaves no error behind.\n authorName: value['authorName'] ?? '',\n authorId: value['authorId'] ?? '',\n coverImage: value['coverImage'] ?? '',\n coverImageAlt: value['coverImageAlt'] ?? '',\n // Here, where both read paths pick it up, so a list card and the routed\n // entry page can each bind the featured video (AGL-2956).\n coverVideo: value['coverVideo'] ?? '',\n seoTitle: value['seoTitle'] ?? '',\n seoDescription: value['seoDescription'] ?? '',\n categoryId: value['categoryId'] ?? '',\n category: value['category'] ?? '',\n tags: Array.isArray(value['tags'])\n ? value['tags'].filter((tag): tag is string => typeof tag === 'string')\n : [],\n /*\n `dateModified`'s source (AGL-2534), and it was missing for the same\n reason `authorName` was — the reason this function's own comment above\n describes.\n\n The console has written `updatedAt` on every save, `page.tsx` reads\n `entry.updatedAt.seconds` to publish `Article.dateModified`, a spec\n asserts the conversion, and two console comments explain why it must not\n track `publishedAt`. Nothing mapped it, so `entry.updatedAt` was\n `undefined` on every entry the loader has ever returned and no published\n article has ever carried a `dateModified` — the freshness signal Google\n reads. The spec passed throughout because it builds its entry object by\n hand and never crosses this boundary.\n\n Mapped HERE rather than beside `publishedAt` at the two call sites: this\n is an ordinary field with no `publishAt` fallback and nothing sorts on\n it, so one place is enough — and one place is what stops the next read\n path from forgetting it.\n */\n updatedAt: value['updatedAt']?.seconds\n ? { seconds: value['updatedAt'].seconds }\n : null,\n }\n}\n\n/**\n * The collection doc's category taxonomy (AGL-582), sanitized: only\n * `{ id, name }` pairs with non-empty strings survive, order preserved.\n *\n * `description` rides along when the author wrote one and is dropped when it\n * is blank or not a string, so the head can tell \"described\" from \"not\n * described\" by truthiness alone — an empty string reaching the metadata\n * would suppress the template screen's description and leave the listing with\n * none at all.\n */\nfunction mapCollectionCategories(value: unknown): CollectionCategory[] {\n if (!Array.isArray(value)) return []\n return value\n .filter(\n (item): item is CollectionCategory =>\n typeof item?.id === 'string' &&\n item.id.trim() !== '' &&\n typeof item?.name === 'string' &&\n item.name.trim() !== '',\n )\n .map((item) => {\n const description =\n typeof item.description === 'string' ? item.description.trim() : ''\n return {\n id: item.id,\n name: item.name,\n ...(description ? { description } : {}),\n }\n })\n}\n\n/**\n * The routed view of a collection DOCUMENT (AGL-551): its name and the\n * template screens its list and entry routes render through.\n *\n * `slug` is the slug that was ASKED FOR rather than the one stored, which is\n * what every caller of this file has always returned — a listing has to build\n * its own URLs out of the segment the reader is standing on.\n */\nfunction mapCollectionDoc(\n collectionDoc: FirebaseFirestore.QueryDocumentSnapshot,\n collectionSlug: string,\n): CollectionContent['collection'] {\n return {\n $id: collectionDoc.id,\n displayName: collectionDoc.get('displayName') ?? collectionSlug,\n slug: collectionSlug,\n templateScreenId: collectionDoc.get('templateScreenId') ?? undefined,\n listScreenId: collectionDoc.get('listScreenId') ?? undefined,\n entryScreenId: collectionDoc.get('entryScreenId') ?? undefined,\n // Normalized HERE rather than at the head (AGL-2536), so an unrecognised\n // stored value can never reach the JSON-LD: an `@type` the vocabulary\n // does not define makes a consumer discard the whole node, costing the\n // page every property it publishes rather than just this one.\n schemaType: normalizeContentSchemaType(collectionDoc.get('schemaType')),\n categories: mapCollectionCategories(collectionDoc.get('categories')),\n }\n}\n\n/**\n * Resolve the author RECORDS a set of entries reference (AGL-2486).\n *\n * Costs ZERO reads when no entry names an `authorId`, which is every site\n * that has not adopted custom authors and every entry written before them —\n * the check is on the ids already in hand, not a probe of the collection. When\n * ids are present it is one `getAll` of the DISTINCT ones, bounded by\n * {@link AUTHORS_MAX_PER_HOST} and by the ≤100-entry page above it, rather\n * than a read per entry.\n *\n * Fail-open, like every other read in this file: an authors read that throws\n * leaves the entries with their legacy `authorName` (or the site entity) and\n * the page renders. A byline is not worth a 500.\n */\nasync function attachEntryAuthors(\n hostId: string,\n entries: CollectionEntrySummary[],\n): Promise<void> {\n const ids = [\n ...new Set(\n entries\n .map((entry) => (entry.authorId ?? '').trim())\n .filter(Boolean),\n ),\n ].slice(0, AUTHORS_MAX_PER_HOST)\n let authors: ContentAuthorRecord[] = []\n if (ids.length) {\n try {\n const authorsRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('authors')\n const snapshots = await firebaseAdmin\n .app()\n .firestore()\n .getAll(...ids.map((id) => authorsRef.doc(id)))\n authors = snapshots\n .map((snapshot) =>\n snapshot.exists\n ? normalizeContentAuthor(snapshot.data(), snapshot.id)\n : null,\n )\n .filter((author): author is ContentAuthorRecord => Boolean(author))\n } catch (error) {\n console.error(error)\n }\n }\n for (const entry of entries) {\n const author = resolveEntryAuthor(entry, authors)\n entry.author = author\n // The byline TEXT is denormalized onto the field everything downstream\n // already reads, so a record-backed author needs no change in the Entry\n // Meta block, the token map, the fallback nodes or the RSS feed. A record\n // WINS over the legacy string on the same entry: picking an author is the\n // more recent statement of who wrote it.\n if (author?.name) entry.authorName = author.name\n }\n}\n\n/**\n * Terminal refusal marker for an entry schedule, the flat-status twin of\n * `publishSchedule.status: 'skipped-unentitled'` on screens (AGL-1185).\n *\n * A field rather than a new `status` value on purpose. `status` is queried\n * (`where('status', 'in', [...])`), rendered by the console, and sorted on by\n * `bundle-timestamps.ts`; adding a member would have meant auditing every one\n * of those readers. A sibling field is invisible to all of them and is read\n * only here.\n */\nconst ENTRY_SCHEDULE_SKIPPED = 'skipped-unentitled'\n\n/** A schedule this entry already had declined, and will not have reconsidered. */\nfunction scheduleAlreadyRefused(\n value: FirebaseFirestore.DocumentData,\n): boolean {\n return value['scheduleStatus'] === ENTRY_SCHEDULE_SKIPPED\n}\n\n/** A scheduled entry whose time has come — before any plan question. */\nexport function isDueScheduled(value: FirebaseFirestore.DocumentData): boolean {\n return (\n value['status'] === 'scheduled' &&\n !scheduleAlreadyRefused(value) &&\n (value['publishAt']?.seconds ?? Number.POSITIVE_INFINITY) * 1000 <=\n Date.now()\n )\n}\n\n/**\n * A scheduled entry still waiting on its time: not due yet, and not refused.\n * Nothing but a render publishes a content entry, so a cache that stored a\n * read holding one would withhold the render that notices it come due — see\n * {@link LiveEntriesRead.pendingSchedule}.\n */\nexport function isPendingScheduled(\n value: FirebaseFirestore.DocumentData,\n): boolean {\n return (\n value['status'] === 'scheduled' &&\n !scheduleAlreadyRefused(value) &&\n !isDueScheduled(value)\n )\n}\n\n/**\n * Is this host's plan allowed to publish on a schedule? (AGL-471 shape.)\n *\n * `React.cache`-deduped per request via `getOrgForHost`, and — this is the\n * part that keeps it off the hot path — every caller below only asks once it\n * has already found a due scheduled entry. A collection with nothing due pays\n * nothing, which is almost every render.\n *\n * THREE answers, not two, and the third is the point. `refused` means we read\n * the plan and it does not carry the entitlement. `unresolved` means we could\n * not find out. Both withhold the entry, but only `refused` may write the\n * terminal marker — burning a schedule permanently on the strength of a\n * hostIndex miss or a transient rejection would destroy a customer's post for\n * a reason that may not be true a second later.\n *\n * Withholding on `unresolved` rather than publishing is what every other\n * entitlement caller on the tenant runtime already does: `apply-publish-schedule`\n * here, and the automation engine's two runners, all pass a possibly\n * undefined org straight into `checkEntitlement`, which resolves a missing\n * plan as free and denies (AGL-247). Opening here instead would make this the\n * one gate in the lib that admits when it cannot see — the exact shape of the\n * free-tier leak `no-plan-gated-entitlement` exists to forbid.\n *\n * The blast radius of withholding is deliberately small: `isLive` answers true\n * for `status: 'published'` before it ever consults this, so an unresolved\n * read hides only the due-scheduled entry, never the published ones, and the\n * next render retries.\n */\nexport type SchedulePermission = 'allowed' | 'refused' | 'unresolved'\n\nexport async function scheduledPublishingPermission(\n hostId: string,\n): Promise<SchedulePermission> {\n try {\n const org = (await getOrgForHost(hostId))?.org\n if (!org) return 'unresolved'\n return checkEntitlement(org, 'scheduledPublishing') ? 'allowed' : 'refused'\n } catch (error) {\n // Caught rather than thrown: the only caller sits inside\n // `getCollectionContent`'s try/catch, which returns an EMPTY collection —\n // so an unhandled rejection here would blank every published entry on the\n // page, not just the scheduled one.\n console.error(error)\n return 'unresolved'\n }\n}\n\n/**\n * Scheduled entries (AGL-123) go live lazily like AGL-61: a due\n * `publishAt` counts as published for this render, and the doc is flipped\n * to `published` fail-open so the state becomes durable.\n *\n * PLAN GATE (AGL-471). `scheduledPublishing` is a Business entitlement, and\n * until now nothing on the entry path checked it: the console let any plan\n * write `status: 'scheduled'`, and this render path published it. Scheduling\n * worked end to end on Free. The screens path has gated this since AGL-471\n * and records its refusal since AGL-1185 — entries were simply never wired\n * to either, which is why the leak was invisible from the screens side.\n *\n * The permission is threaded in rather than resolved here so the org read\n * happens once per call site instead of once per entry.\n *\n * Exported for the one other reader that decides whether an entry is on the\n * site — whether a LINK to it resolves (AGL-3118) — so a link and the page it\n * points at can never disagree about which entries exist.\n */\nexport function isLive(\n value: FirebaseFirestore.DocumentData,\n permission: SchedulePermission,\n): boolean {\n if (value['status'] === 'published') return true\n return permission === 'allowed' && isDueScheduled(value)\n}\n\n/**\n * Make the due state durable — or record that it was refused.\n *\n * The refusal is TERMINAL, for the AGL-1185 reason: left as a bare pending\n * `scheduled`, the entry stays permanently due, so the day the org upgrades\n * to Business the next render publishes it. Content scheduled on a plan that\n * could not honour it, and forgotten, surfacing during an upgrade — exactly\n * when nobody is looking for it. Recording the refusal is what makes it stop\n * being due, and it also stops this path re-reading the org on every\n * subsequent render.\n *\n * Both writes fail open: an error leaves today's state, and the next render\n * retries.\n */\nfunction flipDueEntry(\n docRef: FirebaseFirestore.DocumentReference,\n value: FirebaseFirestore.DocumentData,\n permission: SchedulePermission,\n): void {\n if (value['status'] !== 'scheduled') return\n if (scheduleAlreadyRefused(value)) return\n // `unresolved` writes NOTHING. It withholds this render and leaves the\n // schedule exactly as it found it, so a later render can still publish it.\n if (permission === 'unresolved') return\n if (permission === 'refused') {\n if (!isDueScheduled(value)) return\n docRef\n .update({ scheduleStatus: ENTRY_SCHEDULE_SKIPPED })\n .catch((error) => console.error(error))\n return\n }\n docRef\n .update({ status: 'published', publishedAt: value['publishAt'] })\n .catch((error) => console.error(error))\n}\n\nexport interface CollectionContent {\n collection: {\n $id: string\n displayName: string\n slug: string\n /**\n * Legacy entry-template screen (AGL-105); superseded by\n * `entryScreenId` but still honored when only it is set.\n */\n templateScreenId?: string\n /** List-template screen (AGL-551); `/{collection}` renders through it. */\n listScreenId?: string\n /**\n * Entry-template screen (AGL-551); `/{collection}/{entry}` renders\n * through it with `{{entry.*}}` tokens.\n */\n entryScreenId?: string\n /**\n * What KIND of article this collection publishes (AGL-2536) — the\n * `schema.org` type its entries serialise as. Unset publishes `Article`,\n * which is what every collection published before the setting existed.\n */\n schemaType?: string\n /**\n * Category taxonomy (AGL-582): entries reference these by stable\n * `id`; `name` is the renameable display label.\n */\n categories?: CollectionCategory[]\n } | null\n entries: CollectionEntrySummary[]\n entry: CollectionEntrySummary | null\n /**\n * Whether the read that produced `entries` stopped at\n * {@link COLLECTION_SOURCE_MAX} (AGL-1516). Set on LIST routes only —\n * an entry route reads one document by slug and bounds nothing.\n */\n entriesReachedBound?: boolean\n /**\n * Present ONLY when a verified preview grant revealed an entry the public\n * site withholds (AGL-3205) — never on a public render, and never for an\n * entry that is already live. Its absence is what the preview chrome reads\n * to say \"this one is actually published\", so it must not be set\n * defensively.\n */\n entryPreview?: {\n /** The stored `status` — `scheduled` or `draft`. */\n status: string\n /** The instant it is scheduled for, or null when nothing is scheduled. */\n publishAtSeconds: number | null\n }\n /** List pagination (AGL-620); null for entry pages or unpaginated lists. */\n pagination?: CollectionPagination | null\n /**\n * The category this listing is filtered to (AGL-1321); null on the\n * canonical unfiltered list and on entry pages.\n */\n category?: CollectionRouteCategory | null\n error: unknown\n}\n\n/** The category a `/{collection}/category/{slug}` route addresses (AGL-1321). */\nexport interface CollectionRouteCategory {\n /** The URL segment, normalized — what the canonical link must say. */\n slug: string\n /** Taxonomy id; absent when the segment matched no known category. */\n id?: string\n /** Display label; falls back to the raw segment for an unknown category. */\n name: string\n /**\n * The taxonomy's {@link CollectionCategory.description}, carried onto the\n * route so the head can describe the FILTERED listing rather than inherit\n * the whole collection's description. Absent for an unknown segment, which\n * names no category and therefore has nothing to describe.\n */\n description?: string\n /**\n * Whether the segment resolved against the collection's taxonomy. An\n * unknown category still renders — an empty listing, not a crash — but the\n * page must not invite indexing of a URL that names nothing.\n */\n known: boolean\n}\n\nexport interface CollectionPagination {\n /**\n * 1-based counter for display only (AGL-3219).\n *\n * It is what `{{pagination.page}}` renders and nothing reads it back: a\n * cursor page is addressed by the document it starts after, so this number\n * describes how far a reader has walked rather than where the read began.\n * Hand-edit it in a URL and the label is wrong; the entries are not.\n */\n page: number\n perPage: number\n /**\n * The document id the NEXT (older) page starts after, or `''` when this is\n * the last page (AGL-3219).\n *\n * Set from a `limit(perPage + 1)` probe: the extra row is the only evidence\n * needed that an older page exists, and it costs one document rather than\n * the `count()` aggregation a total used to need. It is also the only\n * honest answer available — see {@link CollectionPagination.totalPages}.\n */\n nextCursor: string\n /** The document id the PREVIOUS (newer) page starts before, or `''`. */\n prevCursor: string\n /**\n * How many pages there are.\n *\n * @deprecated AGL-3219. Still resolved, so a template binding\n * `{{pagination.totalPages}}` keeps rendering, but no longer a promise: a\n * total can only be stated for a collection that fits inside one read, and\n * past that it is absent rather than wrong. It used to be derived from a\n * `count()` over a set the listing's two reads disagreed about, which is\n * how the page 10/11 seam came to repeat an entry. Bind\n * {@link CollectionPagination.nextCursor} instead — `''` means there is\n * nothing older, which is the question a pager is actually asking.\n */\n totalPages?: number\n /** @deprecated AGL-3219, with {@link CollectionPagination.totalPages}. */\n totalEntries?: number\n /**\n * Where `entries` begins in the collection's own order (AGL-3213): 0 for a\n * listing served from the cached read, and the page's own offset for one\n * served by a window read past that read's bound.\n *\n * Both windowing sites subtract it — `collectionEntriesPageWindow` on the\n * way into props, and the Collection entries block on the way into compose.\n * Without it a windowed listing renders empty, because every one of them\n * slices `[(page - 1) * perPage, …)` on the premise that `entries` starts at\n * the beginning of the collection.\n */\n windowStart?: number\n}\n\n/**\n * A bounded read of a collection's live entries (AGL-1516).\n *\n * `reachedBound` is a fact about the QUERY, not about `entries`, and the two\n * genuinely differ: the query asks for `status in ['published', 'scheduled']`\n * and the filter below then drops everything not live yet, so a read that came\n * back holding all {@link COLLECTION_SOURCE_MAX} docs can hand back fewer.\n * Counting the survivors — which is all a downstream consumer can do — reads\n * that as a complete collection, and the one thing a truncated read must never\n * be allowed to claim is completeness.\n */\ninterface LiveEntriesRead {\n entries: CollectionEntrySummary[]\n /** The query came back holding its own `.limit()`. */\n reachedBound: boolean\n /**\n * The read saw a `scheduled` entry whose `publishAt` has NOT arrived and\n * which has not been terminally refused — a schedule this collection is\n * still waiting on.\n *\n * Nothing promotes a content entry on a beat: `publish-schedule-job.ts` is\n * screens-only, so `isLive`/`flipDueEntry` running during a render is the\n * entire mechanism. A cached source therefore does not merely serve stale\n * entries, it withholds the render that would have published one, for as\n * long as the entry stays cached. This is what lets the cache decline to\n * store exactly those collections, so a schedule keeps landing on the\n * render window rather than on the TTL.\n */\n pendingSchedule: boolean\n}\n\n/**\n * The fields a LISTING read of an entry needs — the field mask on the query\n * below (AGL-3213).\n *\n * The point of the mask is the field that is NOT in it. `body` is the whole\n * post, and `mapEntryFields` has never mapped it on this path: a list card\n * binds a title, an excerpt, a cover and a byline, and the routed entry page\n * reads its one document separately. So the markdown of up to\n * {@link COLLECTION_SOURCE_MAX} posts crossed the wire, was parsed out of the\n * response, and was dropped one function later — on every fill of a cache\n * that every listing address, every \"Latest posts\" rail, the feed and the\n * author page share. A changelog is the worst case and also the common one.\n *\n * Firestore bills the document read either way, so this buys no reads; it\n * buys egress and the JSON parse, which is the part of a collection render\n * that grows with how much people have written.\n *\n * Site search is unaffected and must stay that way: it matches on `body`\n * through its OWN query in `apps/tenant/utils/search-content.ts`, which this\n * mask does not touch.\n *\n * ⛔ A reader added to `mapEntryFields` or to the liveness/schedule helpers\n * must be added HERE in the same edit. A field left out does not error — it\n * arrives `undefined`, which is exactly how `authorName` and `updatedAt` went\n * missing for months (AGL-2486, AGL-2534). The four schedule fields are\n * listed first for that reason: `status`, `publishAt` and `scheduleStatus`\n * decide whether an entry is live at all, and `flipDueEntry` WRITES\n * `publishAt` back as `publishedAt`, so a mask that dropped it would publish\n * a due entry with no date.\n */\nconst LIVE_ENTRY_FIELDS = [\n 'status',\n 'publishAt',\n 'publishedAt',\n 'scheduleStatus',\n 'title',\n 'slug',\n 'excerpt',\n 'authorName',\n 'authorId',\n 'coverImage',\n 'coverImageAlt',\n 'coverVideo',\n 'seoTitle',\n 'seoDescription',\n 'categoryId',\n 'category',\n 'tags',\n 'updatedAt',\n] as const\n\n/**\n * Most SCHEDULED entries one live read considers (AGL-3213).\n *\n * Read by its own query rather than taken from the dated page below, because\n * a schedule is invisible to that page: scheduling writes `publishAt` and\n * never `publishedAt`, and `flipDueEntry` during a render is the only thing\n * that publishes one. A schedule the read misses is a post that never goes\n * out. The set is small by nature — a hundred pending schedules on one\n * collection is already an unusual editorial calendar.\n */\nconst SCHEDULED_SOURCE_MAX = 100\n\n/** Everything a public listing may show, before any ordering. */\nfunction liveEntriesBase(\n entriesRef: FirebaseFirestore.CollectionReference,\n): FirebaseFirestore.Query {\n return (\n entriesRef\n .where('status', 'in', ['published', 'scheduled'])\n // Everything this path reads, and nothing else (AGL-3213) — see\n // {@link LIVE_ENTRY_FIELDS}.\n .select(...LIVE_ENTRY_FIELDS)\n )\n}\n\n/**\n * The documents a live read considers, in the order the site shows them\n * (AGL-3213).\n *\n * ## Why this is ordered now, and why ordering alone would have broken it\n *\n * It was `where(status).limit(100)` with NO `orderBy`, sorted in memory\n * afterwards. That is not the newest hundred — it is a hundred documents in\n * NAME order, then sorted. Under the bound the two agree, because a hundred\n * out of a hundred is everything; past it they diverge completely. This site's\n * own changelog reached 166 live entries and its listing showed an arbitrary\n * hundred of them, chosen by document id, with 66 releases reachable only at\n * their own URLs.\n *\n * The comment that used to sit here was right about `orderBy`, though:\n * Firestore returns only documents that HAVE the ordered field, so a dated\n * read does not mis-sort an entry without a date — it hides it. Two of those\n * exist and both matter:\n *\n * A SCHEDULE carries `publishAt` and no `publishedAt` until it goes out.\n * Ordering alone would have stopped scheduled posts publishing\n * at all, silently, because nothing else publishes them.\n * AN IMPORT restores whatever the bundle carried, and\n * `/api/hosts/resources` validates no field for presence\n * either — so a published entry with no `publishedAt` exists.\n *\n * So it is three queries, in the shape the console's sorted window uses\n * (AGL-2853): the DATED page, every SCHEDULE, and — only when the dated page\n * came back SHORT, which is the server's own proof that the collection fits\n * inside the bound — a scan for live entries carrying no date. Past the bound\n * an undated entry sorts after every dated one by definition, so it belongs to\n * the tail pages, which are served by their own window read.\n */\nasync function readLiveEntryDocs(\n entriesRef: FirebaseFirestore.CollectionReference,\n): Promise<{\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n reachedBound: boolean\n}> {\n try {\n const dated = await liveEntriesBase(entriesRef)\n .orderBy('publishedAt', 'desc')\n // The document name breaks ties, so two entries published in the same\n // second cannot swap places between two reads and move a page boundary\n // under a reader. Descending to match the date: a composite index ends\n // with `__name__` in the last field's direction, which makes this the\n // `(status, publishedAt DESC)` index the console's table already needs.\n .orderBy('__name__', 'desc')\n // Named rather than literal (AGL-1516): a search index has to be able to\n // say \"this read reached its bound\", and it can only do that against a\n // bound it shares with the query. `collectionSourceReachedBound` reads\n // the same constant.\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n\n const scheduled = await entriesRef\n .where('status', '==', 'scheduled')\n .select(...LIVE_ENTRY_FIELDS)\n .limit(SCHEDULED_SOURCE_MAX)\n .get()\n\n /*\n * EITHER read stopping at its own limit means entries went unseen.\n *\n * The dated page is the usual one. The schedule page is the case a dated\n * read alone cannot even detect: a collection holding nothing but pending\n * schedules returns ZERO dated documents, so a bound measured only there\n * would report a complete read of an empty collection — and \"nothing is\n * live here\" is the claim that takes a listing off the site (AGL-3101).\n */\n const reachedBound =\n dated.docs.length >= COLLECTION_SOURCE_MAX ||\n scheduled.docs.length >= SCHEDULED_SOURCE_MAX\n\n const undated = reachedBound\n ? []\n : (\n await liveEntriesBase(entriesRef)\n .orderBy('__name__')\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n ).docs.filter((entryDoc) => !entryDoc.get('publishedAt'))\n\n const seen = new Set<string>()\n const docs: FirebaseFirestore.QueryDocumentSnapshot[] = []\n for (const entryDoc of [...dated.docs, ...scheduled.docs, ...undated]) {\n if (seen.has(entryDoc.id)) continue\n seen.add(entryDoc.id)\n docs.push(entryDoc)\n }\n return { docs, reachedBound }\n } catch (error) {\n /*\n * FAIL SOFT TO THE UNORDERED READ.\n *\n * The ordered query needs the `(status, publishedAt DESC)` composite\n * index. Indexes do NOT ship with a promotion — RELEASING.md deploys them\n * by hand afterwards — so the window between the code landing and the\n * index existing has to degrade rather than break. An arbitrary hundred\n * is a bad listing; a 500 is a customer's blog down.\n */\n console.error(error)\n const unordered = await liveEntriesBase(entriesRef)\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n return {\n docs: unordered.docs,\n reachedBound: unordered.docs.length >= COLLECTION_SOURCE_MAX,\n }\n }\n}\n\n/** The live entries among `docs`, newest first, publishing any that came due. */\nfunction toLiveEntries(\n docs: readonly FirebaseFirestore.QueryDocumentSnapshot[],\n permission: SchedulePermission,\n): CollectionEntrySummary[] {\n return (\n docs\n .filter((entryDoc) => isLive(entryDoc.data(), permission))\n .map((entryDoc) => {\n const value = entryDoc.data()\n flipDueEntry(entryDoc.ref, value, permission)\n return {\n $id: entryDoc.id,\n title: value['title'] ?? entryDoc.id,\n slug: value['slug'] ?? entryDoc.id,\n ...mapEntryFields(value),\n publishedAt: (value['publishedAt'] ?? value['publishAt'])\n ? {\n seconds: (value['publishedAt'] ?? value['publishAt']).seconds,\n }\n : null,\n }\n })\n // An entry carrying no date at all sorts last rather than to 1970 —\n // the same place the query's own order puts it.\n .sort(\n (a, b) => (b.publishedAt?.seconds ?? 0) - (a.publishedAt?.seconds ?? 0),\n )\n )\n}\n\n/**\n * Fetches a collection's live entries (newest first), shared by the route\n * loader and the compose-time Collection entries block (AGL-551).\n */\nasync function listLiveEntries(\n entriesRef: FirebaseFirestore.CollectionReference,\n hostId: string,\n): Promise<LiveEntriesRead> {\n const { docs, reachedBound } = await readLiveEntryDocs(entriesRef)\n\n // Ask the plan question ONLY if something is actually due (AGL-471). A\n // collection with no due schedule — almost every render — never reads the\n // org at all.\n const due = docs.filter((entryDoc) => isDueScheduled(entryDoc.data()))\n const permission: SchedulePermission = due.length\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n\n // Record the terminal refusal on its own pass, because a refused entry is\n // NOT live and so never reaches the `flipDueEntry` inside the map below.\n // Without this the entry stays due forever: excluded from every render, and\n // re-reading the org on each one.\n if (permission !== 'allowed') {\n for (const entryDoc of due) {\n flipDueEntry(entryDoc.ref, entryDoc.data(), permission)\n }\n }\n\n // Measured on the RAW docs, before the liveness filter (AGL-1516): a\n // not-yet-due entry is filtered out one line down, so this is the last\n // place that can see one at all.\n const pendingSchedule = docs.some((entryDoc) =>\n isPendingScheduled(entryDoc.data()),\n )\n\n const entries = toLiveEntries(docs, permission)\n\n return {\n entries,\n reachedBound,\n pendingSchedule,\n }\n}\n\n/** One page of a listing, and whether anything older follows it. */\ninterface CollectionListingPage {\n entries: CollectionEntrySummary[]\n /** The `limit(perPage + 1)` probe found an extra row. */\n hasMore: boolean\n}\n\n/**\n * One page of a listing that starts PAST the cached read (AGL-3213),\n * addressed by the document it continues from rather than by a position\n * (AGL-3219).\n *\n * Uncached on purpose, and it is the only read on the collection path that is.\n * The cached source exists because `/blog`, every listing address, the feed\n * and every \"Latest posts\" rail want the same first hundred entries\n * (AGL-1302); a page past that bound wants ten entries nobody else is asking\n * for, and its own ISR entry is already the cache for them.\n *\n * ## Why a cursor, and not the offset this replaces\n *\n * `.offset(n)` asks for a POSITION, and a listing takes its inserts at the\n * head, so every publish moves every position by one. The head pages and this\n * read are cached under different policies and revalidated by different\n * triggers — the publish fan-out refreshes the head eagerly and deliberately\n * stops there — so the two sides were routinely describing the collection as\n * it stood at two different moments, and the seam between them repeated an\n * entry or hid one for as long as the slower side lagged. `v1.0.0-beta.147`\n * shipped and `/changelog` showed `v1-0-0-beta-36` as both the last entry of\n * page 10 and the first of page 11.\n *\n * `startAfter(doc)` asks a question whose answer does not move: what follows\n * THIS entry. Publish a hundred entries at the head and this page returns the\n * same ten. The seam cannot drift because there is no longer a number on\n * either side of it to disagree about.\n *\n * `offset` also billed every document it skipped; a cursor bills none of them.\n */\nasync function readCollectionListingPage(options: {\n hostId: string\n collectionSlug: string\n /** Start after this document id — the older direction. */\n after?: string\n /** Start before this document id — the newer direction. */\n before?: string\n limit: number\n}): Promise<CollectionListingPage | null> {\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) return null\n const entriesRef = collectionDoc.ref.collection('entries')\n\n const cursorId = options.before || options.after\n if (!cursorId) return null\n // A direct get, not a `where('slug', ...)` query: the cursor IS the\n // document name, which is the second field the read orders on, so the\n // snapshot it needs is one document rather than an index lookup.\n const cursorDoc = await entriesRef.doc(cursorId).get()\n // A cursor naming a document that no longer exists — deleted, or a URL\n // someone kept — cannot be positioned against. Null sends the caller back\n // to the cached head, which is a page the reader can act on.\n if (!cursorDoc.exists) return null\n\n // Backwards is the same query read the other way up, so both directions\n // rest on an index the project already deploys: `(status, publishedAt)`\n // exists ascending and descending both.\n const backwards = Boolean(options.before)\n const snapshot = await liveEntriesBase(entriesRef)\n .orderBy('publishedAt', backwards ? 'asc' : 'desc')\n .orderBy('__name__', backwards ? 'asc' : 'desc')\n .startAfter(cursorDoc)\n // One more than the page: the extra row is the whole of \"is there\n // another page\", and it replaces a `count()` over the collection.\n .limit(options.limit + 1)\n .get()\n\n const hasMore = snapshot.docs.length > options.limit\n const pageDocs = snapshot.docs.slice(0, options.limit)\n // Read backwards, the newest of the page came back last.\n const docs = backwards ? [...pageDocs].reverse() : pageDocs\n\n const due = docs.filter((entryDoc) => isDueScheduled(entryDoc.data()))\n const permission: SchedulePermission = due.length\n ? await scheduledPublishingPermission(options.hostId)\n : 'allowed'\n if (permission !== 'allowed') {\n for (const entryDoc of due) {\n flipDueEntry(entryDoc.ref, entryDoc.data(), permission)\n }\n }\n const entries = toLiveEntries(docs, permission)\n // The byline reads `authorName`, which a record-backed author only has\n // once resolved — the cached source does this for the entries it holds,\n // and a windowed page holds entries it never saw (AGL-2486).\n await attachEntryAuthors(options.hostId, entries)\n return { entries, hasMore }\n } catch (error) {\n // Fail open to the cached head rather than to a 500: the caller keeps\n // whatever it already had, which is a page the reader has seen before\n // rather than an error they cannot get past.\n console.error(error)\n return null\n }\n}\n\n/**\n * The cursor a retired `/{collection}/page/{n}` address redirects onto\n * (AGL-3219).\n *\n * This is the ONE place a position is still resolved, and it is deliberately\n * the only one: a 301 is served once, the reader lands on an address that\n * cannot drift afterwards, and whatever the position meant at that instant is\n * the page they would have got anyway. Everything downstream is cursors.\n *\n * Keys only — `select()` with no fields asks Firestore for document names and\n * nothing else — so the skipped documents are billed at the cheapest rate the\n * offset can be had for. Returns `''` when the position is past the end,\n * which is a 404 rather than a redirect to nowhere.\n */\nexport async function resolveCollectionPageCursor(options: {\n hostId: string\n collectionSlug: string\n /** 1-based page whose PREVIOUS entry is the cursor. */\n page: number\n perPage: number\n}): Promise<string> {\n const skip = (options.page - 1) * options.perPage - 1\n if (!Number.isFinite(skip) || skip < 0) return ''\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) return ''\n const snapshot = await collectionDoc.ref\n .collection('entries')\n .where('status', 'in', ['published', 'scheduled'])\n .select()\n .orderBy('publishedAt', 'desc')\n .orderBy('__name__', 'desc')\n .offset(skip)\n .limit(1)\n .get()\n return snapshot.docs[0]?.id ?? ''\n } catch (error) {\n console.error(error)\n return ''\n }\n}\n\n/** Compose-time view of a collection: its live entries and its taxonomy. */\nexport interface PublishedCollectionSource {\n /**\n * The collection DOCUMENT this source was read from — its display name and\n * its template screen ids — or null when the slug names no content\n * collection.\n *\n * Carried so a routed listing can be served ENTIRELY from this cached\n * source. `getCollectionContent` used to resolve the same document itself\n * and then read the same entries again uncached, which meant `/blog`,\n * every `/blog/page/{n}`, every `/blog/category/{slug}` and the RSS feed\n * each paid a full collection read per regeneration while the identical\n * data already sat in this cache for every OTHER page on the site.\n */\n collection: CollectionContent['collection']\n entries: CollectionEntrySummary[]\n categories: CollectionCategory[]\n /**\n * Whether the read saw a schedule it is still waiting on — see\n * {@link LiveEntriesRead.pendingSchedule}. Never stored, only consulted by\n * the cache above, so no consumer has to know about it.\n */\n pendingSchedule?: boolean\n /**\n * Whether the entries read stopped at {@link COLLECTION_SOURCE_MAX}\n * (AGL-1516) — carried out of the loader because `entries.length` cannot\n * answer it once the liveness filter has run. Fail-open paths report\n * `false`: an empty result is not a bounded read, and describing it as one\n * would tell a reader their search covered less than it did.\n */\n reachedBound: boolean\n}\n\n/**\n * Published entries + category taxonomy for a collection resolved by slug —\n * the data source of the Collection entries block on arbitrary screens\n * (AGL-551/582). Fail-open: errors and unknown slugs resolve to an empty\n * list so a renamed collection never takes a published screen down.\n */\nexport async function getPublishedCollectionSource(options: {\n hostId: string\n collectionSlug: string\n}): Promise<PublishedCollectionSource> {\n try {\n return await withRenderCache({\n key: [\n 'tenant-collection-source',\n options.hostId,\n options.collectionSlug,\n ],\n revalidate: COLLECTION_SOURCE_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readPublishedCollectionSource(options),\n // A collection with a schedule still pending is SERVED but not STORED.\n //\n // No beat publishes a content entry — `flipDueEntry` during a render is\n // the whole mechanism — so storing this source would suppress the very\n // renders that would have noticed the entry coming due, and the post\n // would wait out the TTL rather than land at its time. Declining to\n // store leaves those collections exactly as uncached as they were\n // before this cache existed, which is the only cost this cache is not\n // allowed to reduce.\n //\n // Also refuses a collection that resolved to nothing, for the reason\n // `withRenderCache` states about negatives generally: a slug that\n // misses once must not miss for an hour.\n store: (value) => Boolean(value.collection) && !value.pendingSchedule,\n })\n } catch (error) {\n console.error(error)\n return readPublishedCollectionSource(options)\n }\n}\n\nasync function readPublishedCollectionSource(\n options: {\n hostId: string\n collectionSlug: string\n },\n): Promise<PublishedCollectionSource> {\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) {\n return {\n collection: null,\n entries: [],\n categories: [],\n reachedBound: false,\n }\n }\n const { entries, reachedBound, pendingSchedule } = await listLiveEntries(\n collectionDoc.ref.collection('entries'),\n options.hostId,\n )\n // The compose-time source feeds the Collection entries block, whose byline\n // reads `authorName` — so a record-backed author has to be resolved here\n // too, or the block prints nothing for the entries a list page shows\n // (AGL-2486). This result is the cached one, which is what keeps the extra\n // read amortized across every page of the site that carries the block.\n await attachEntryAuthors(options.hostId, entries)\n return {\n collection: mapCollectionDoc(collectionDoc, options.collectionSlug),\n entries,\n categories: mapCollectionCategories(collectionDoc.get('categories')),\n reachedBound,\n ...(pendingSchedule ? { pendingSchedule: true } : {}),\n }\n } catch (error) {\n console.error(error)\n return {\n collection: null,\n entries: [],\n categories: [],\n reachedBound: false,\n }\n }\n}\n\n/** Entries-only view of {@link getPublishedCollectionSource} (AGL-551). */\nexport async function getPublishedCollectionEntries(options: {\n hostId: string\n collectionSlug: string\n}): Promise<CollectionEntrySummary[]> {\n return (await getPublishedCollectionSource(options)).entries\n}\n\n/**\n * Narrows a listing to its routed category and stamps its pagination\n * (AGL-1321 / AGL-620).\n *\n * Category first, ALWAYS: the two have to describe the same set. Counting\n * pages over the whole collection and then filtering would advertise pages\n * that render empty and hide entries that exist.\n *\n * `entriesReachedBound` is read by the caller BEFORE this runs, because a\n * category route hands its already-narrowed entries to compose and the\n * entries block's \"measure the raw set, not the filtered one\" rule then has\n * nothing raw left to measure (AGL-1516).\n */\nfunction applyCategoryAndPagination(\n data: CollectionContent,\n options: {\n page?: number\n perPage?: number\n categorySlug?: string\n },\n listing?: {\n /** Where `data.entries` begins in that collection's order. */\n windowStart?: number\n /** The shared read came back holding its own limit. */\n reachedBound?: boolean\n /**\n * The `perPage + 1` probe of a CURSOR page found an extra row — set only\n * when this listing was served by one, because only that read can answer\n * it (AGL-3219).\n */\n cursorHasMore?: boolean\n },\n): void {\n const { page = 1, perPage } = options\n const routedCategory = (options.categorySlug ?? '').trim()\n if (routedCategory) {\n const match = resolveCollectionCategoryBySlug(\n data.collection?.categories,\n routedCategory,\n )\n data.category = {\n slug: collectionCategorySlug(routedCategory),\n ...(match ? { id: match.id } : {}),\n name: match?.name ?? routedCategory,\n ...(match?.description ? { description: match.description } : {}),\n known: Boolean(match),\n }\n data.entries = data.entries.filter((entry) =>\n entryMatchesCategoryRoute(\n entry,\n { slug: routedCategory, ...(match ? { category: match } : {}) },\n data.collection?.categories,\n ),\n )\n }\n\n if (perPage && perPage > 0) {\n /*\n * The total is the COLLECTION's, not the read's (AGL-3213).\n *\n * `entries.length` was the count, and on a collection inside the bound it\n * still is — the two are the same number there. Past the bound it is the\n * size of the window, which is how `/changelog` advertised \"Page 10 of\n * 10\" while holding 166 entries and linking to 100 of them.\n *\n * A ROUTED CATEGORY keeps counting its own entries, because that is the\n * only honest number available here: the narrowing happens in memory over\n * the cached head, so both the count and the listing describe the same\n * bounded set. Paging a category past the bound needs its own ordered\n * query and a `(categoryId, status, publishedAt DESC)` index; until then\n * a category of a very large collection is capped, and says so by\n * agreeing with what it shows.\n */\n const windowStart = Number(listing?.windowStart)\n const windowed = Number.isFinite(windowStart) && windowStart > 0\n\n /*\n * The cursors, which are the pager's real answer (AGL-3219).\n *\n * `data.entries` is either EXACTLY this page — a cursor read — or the\n * whole cached head, which every listing address shares and each slices\n * its own page out of. So the page's own last entry is at the end of the\n * array in the first case and at `page * perPage - 1` in the second, and\n * the cursor is that entry's document id.\n */\n const pageEnd = windowed ? data.entries.length : page * perPage\n const last = data.entries[pageEnd - 1]\n const first = data.entries[windowed ? 0 : (page - 1) * perPage]\n\n /*\n * Is there an older page?\n *\n * A cursor page KNOWS, because its read asked for one more entry than it\n * needed and the extra row came back. The head has to reason: either it\n * is holding more entries than this page shows, or it is holding all it\n * could read and the read stopped at its own bound, which is the one\n * thing `entries.length` can never tell you about the collection.\n */\n const hasMore =\n listing?.cursorHasMore ??\n (data.entries.length > pageEnd ||\n (Boolean(listing?.reachedBound) && !routedCategory))\n\n /*\n * The deprecated total, stated ONLY where it can be true.\n *\n * A collection that fits inside one read can be counted, and a template\n * binding `{{pagination.totalPages}}` keeps rendering the number it\n * always did. Past the bound there is no honest total — the count and\n * the listing were reading the collection at two different moments, which\n * is what made the seam repeat an entry — so it is left absent rather\n * than asserted. A routed category counts its own narrowed set, which is\n * bounded by the same head and therefore countable.\n */\n const countable = routedCategory || !listing?.reachedBound\n const totalEntries = countable ? data.entries.length : undefined\n\n data.pagination = {\n page,\n perPage,\n nextCursor: hasMore && last?.$id ? last.$id : '',\n prevCursor: page > 1 && first?.$id ? first.$id : '',\n ...(totalEntries === undefined\n ? {}\n : {\n totalEntries,\n totalPages: collectionTotalPages(totalEntries, perPage),\n }),\n ...(windowed ? { windowStart } : {}),\n }\n }\n}\n\n/**\n * Resolves a non-screen path against the host's content collections\n * (Content Collections & Blog): `/{collectionSlug}` returns the published\n * entry list, `/{collectionSlug}/{entrySlug}` one entry. Fail-open — errors\n * resolve to `collection: null` and the caller 404s.\n *\n * A listing resolves only for a collection with a live entry (AGL-3101); one\n * with nothing live answers `collection: null` as well, so its listing, feed\n * and markdown twin are not public until its first entry is.\n */\nexport async function getCollectionContent(options: {\n hostId: string\n collectionSlug: string\n entrySlug?: string\n /**\n * 1-based list page (AGL-620), now a DISPLAY counter (AGL-3219): it labels\n * the page the reader is on and no read is positioned from it. The entries\n * come from `after`/`before`, or from the head of the cached source when\n * neither is set.\n */\n page?: number\n /** Entries per page (AGL-620); when set the list is paginated. */\n perPage?: number\n /**\n * Continue AFTER this entry's document id — the older direction (AGL-3219).\n *\n * What `/{collection}?after={id}` carries. A page defined this way does not\n * move when something is published above it, which is the whole reason the\n * addresses stopped being positions.\n */\n after?: string\n /** Continue BEFORE this entry's document id — the newer direction. */\n before?: string\n /**\n * Category segment of `/{collection}/category/{slug}` (AGL-1321). Filters\n * the listing before pagination is computed, so page counts and the page\n * windows describe the FILTERED set rather than the whole collection.\n */\n categorySlug?: string\n /**\n * Reveal the named entry even though the public site withholds it — the\n * live-site preview of a scheduled post (AGL-3205).\n *\n * ⛔ A VERIFIED GRANT, NOT A REQUEST. The caller passes `true` only after a\n * signed preview token has been checked against the resolved hostId AND\n * this exact `collectionSlug`/`entrySlug` (`verifyCollectionPreviewToken`).\n * The token format stays in the app that receives the URL; what crosses into\n * this lib is the verdict, so nothing here can be tricked by a payload's own\n * spelling of which entry it names.\n *\n * ENTRY ROUTES ONLY, and one entry at a time. It is ignored on a list route\n * — a preview of one post must not add it to `/blog`, to the feed, or to a\n * Collection entries block on any other page, all of which read the SHARED\n * cached source. Nothing it touches is cached at all.\n *\n * The preview render also writes NOTHING: see the `flipDueEntry` guard\n * below. Publishing a schedule stays the sole business of a public render.\n */\n previewUnpublishedEntry?: boolean\n}): Promise<CollectionContent> {\n const { hostId, collectionSlug, entrySlug } = options\n // Never on a list route, whatever the caller passed: `entrySlug` is what\n // makes the grant addressable at all, and the list is the shared cached\n // read this must stay out of.\n const preview = Boolean(options.previewUnpublishedEntry) && Boolean(entrySlug)\n const data: CollectionContent = {\n collection: null,\n entries: [],\n entry: null,\n pagination: null,\n category: null,\n error: null,\n }\n try {\n // A LIST route is served entirely from the CACHED source, which every\n // other page on the site already shares. It used to resolve the\n // collection and re-read its entries here, uncached, on every\n // regeneration of every listing address — `/blog`, nine `/blog/page/{n}`,\n // one `/blog/category/{slug}` per category, and the RSS feed — so a\n // collection of N live entries cost `1 + N + authors` reads per address\n // per window, for data byte-identical to what the cache was already\n // holding for the home page's \"Latest posts\" rail.\n //\n // An ENTRY route stays where it was: it reads ONE document by slug and\n // has nothing to share.\n if (!entrySlug) {\n const source = await getPublishedCollectionSource({\n hostId,\n collectionSlug,\n })\n if (!source.collection) return data\n // A collection is not a PAGE until something in it is live (AGL-3101).\n // Without this, creating one publishes `/{slug}` at once — an empty\n // listing with a feed and a markdown twin, before a word of it is\n // written. Answering it as no collection makes every listing address\n // 404 the way an unknown slug does, until the first entry is published\n // or its schedule comes due.\n //\n // Read off the UNFILTERED live set, before the category narrows it, so\n // an empty category of a live collection still renders. A read that\n // stopped at its bound cannot prove the collection empty — the live\n // entries may be past it — so that one keeps its listing. The rule\n // lives here and not in the source above: the source also feeds the\n // Collection entries block and the author page, and to them an empty\n // collection is an empty list, not a missing one.\n if (!source.entries.length && !source.reachedBound) return data\n data.collection = source.collection\n data.entries = source.entries\n data.entriesReachedBound = source.reachedBound\n /*\n * A page that starts past the cached read is served by its own window\n * (AGL-3213). Everything before that point comes out of the shared\n * source, so the pages a reader actually visits stay free.\n *\n * Only the unfiltered listing: a category route narrows in memory over\n * the same head, and a window read of the whole collection would hand\n * it ten entries of which any number may belong to another category.\n */\n const { page = 1, perPage } = options\n const cursor = (options.after ?? options.before ?? '').trim()\n let windowStart = 0\n let cursored: CollectionListingPage | null = null\n /*\n * A cursor address is served by its own read (AGL-3219). Everything\n * reachable without one comes out of the shared source, so the pages a\n * reader actually visits stay free.\n *\n * Only the unfiltered listing: a category route narrows in memory over\n * the same head, and a cursor read of the whole collection would hand it\n * ten entries of which any number belong to another category.\n */\n if (perPage && perPage > 0 && cursor && !(options.categorySlug ?? '').trim()) {\n cursored = await readCollectionListingPage({\n hostId,\n collectionSlug,\n ...(options.before ? { before: options.before } : { after: cursor }),\n limit: perPage,\n })\n // Null means the cursor read failed, or named a document that is\n // gone. The cached head is the better answer than an empty page: the\n // reader sees the newest entries rather than nothing at all.\n if (cursored) {\n data.entries = cursored.entries\n /*\n * `entries` is now EXACTLY this page, and both windowing sites\n * slice `[(page - 1) * perPage, …)`. Declaring the window to start\n * at that same offset makes their subtraction come out at zero, so\n * they take the page whole.\n *\n * Both sides of the subtraction are built from the same `page`, so\n * a hand-edited counter in a URL cancels itself out: the label is\n * wrong and the entries are still this cursor's page.\n */\n windowStart = (page - 1) * perPage\n }\n }\n applyCategoryAndPagination(data, options, {\n windowStart,\n reachedBound: source.reachedBound,\n ...(cursored ? { cursorHasMore: cursored.hasMore } : {}),\n })\n return data\n }\n\n const collectionDoc = await findContentCollection(hostId, collectionSlug)\n if (!collectionDoc) return data\n data.collection = mapCollectionDoc(collectionDoc, collectionSlug)\n\n const entryQuery = await collectionDoc.ref\n .collection('entries')\n .where('slug', '==', entrySlug)\n .limit(5)\n .get()\n // Same two-step as `listLiveEntries`: only pay for the org read when\n // something is due, and record the refusal on its own pass because a\n // refused entry never becomes the `entryDoc` below (AGL-471).\n //\n // A PREVIEW RENDER SKIPS BOTH WRITES (AGL-3205). `flipDueEntry` is the\n // entire publishing mechanism for a content entry, and it is a mechanism\n // that belongs to the PUBLIC render: a preview is one person looking at\n // one post through a link, and it must not be able to publish it, nor to\n // burn its schedule with the terminal refusal marker. Skipping the writes\n // costs nothing — the next public render asks the same questions and\n // writes the same answers — and it is what keeps \"nothing but a render\n // publishes a content entry\" true of renders anybody can reach.\n const dueHere = entryQuery.docs.filter((docSnapshot) =>\n isDueScheduled(docSnapshot.data()),\n )\n const permission: SchedulePermission = dueHere.length\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n if (permission !== 'allowed' && !preview) {\n for (const docSnapshot of dueHere) {\n flipDueEntry(docSnapshot.ref, docSnapshot.data(), permission)\n }\n }\n /**\n * What a grant reveals: an entry this collection HOLDS and the site does\n * not serve.\n *\n * Deliberately not routed through `isLive`, which is the public answer and\n * has one other reader (`entry-link-routes`) whose whole job is to agree\n * with it. A second, wider answer inside it would make a LINK to a\n * previewed post resolve on the public site.\n *\n * Every status but `published` qualifies, which is `draft` as well as\n * `scheduled`: the ask is \"let me see it before it goes out\", and a post\n * is most worth looking at before its schedule is set. The blast radius is\n * the same either way — one entry, named in the signature, on one host.\n */\n const entryDoc =\n entryQuery.docs.find((docSnapshot) =>\n isLive(docSnapshot.data(), permission),\n ) ?? (preview ? entryQuery.docs[0] : undefined)\n if (entryDoc) {\n const value = entryDoc.data()\n if (!preview) flipDueEntry(entryDoc.ref, value, permission)\n if (preview && !isLive(value, permission)) {\n // The facts the preview chrome states back to the reader, so the page\n // cannot be mistaken for the published post. Read from the stored\n // document rather than inferred from the render.\n data.entryPreview = {\n status: String(value['status'] ?? 'draft'),\n publishAtSeconds:\n typeof value['publishAt']?.seconds === 'number'\n ? value['publishAt'].seconds\n : null,\n }\n }\n data.entry = {\n $id: entryDoc.id,\n title: value['title'] ?? entrySlug,\n slug: entrySlug,\n body: value['body'] ?? '',\n ...mapEntryFields(value),\n publishedAt: (value['publishedAt'] ?? value['publishAt'])\n ? {\n seconds: (value['publishedAt'] ?? value['publishAt']).seconds,\n }\n : null,\n }\n await attachEntryAuthors(hostId, [data.entry])\n }\n } catch (error) {\n console.error(error)\n data.error = error\n }\n return data\n}\n\nexport default getCollectionContent\n"],"names":["AUTHORS_MAX_PER_HOST","collectionCategorySlug","checkEntitlement","COLLECTION_SOURCE_MAX","collectionTotalPages","entryMatchesCategoryRoute","hostCollectionKind","normalizeContentAuthor","normalizeContentSchemaType","resolveCollectionCategoryBySlug","resolveEntryAuthor","firebaseAdmin","getOrgForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","COLLECTION_SOURCE_TTL_SECONDS","findContentCollection","hostId","collectionSlug","matches","app","firestore","collection","doc","where","limit","get","docs","find","docSnapshot","data","mapEntryFields","value","excerpt","authorName","authorId","coverImage","coverImageAlt","coverVideo","seoTitle","seoDescription","categoryId","category","tags","Array","isArray","filter","tag","updatedAt","seconds","mapCollectionCategories","item","id","trim","name","map","description","mapCollectionDoc","collectionDoc","$id","displayName","slug","templateScreenId","undefined","listScreenId","entryScreenId","schemaType","categories","attachEntryAuthors","entries","ids","Set","entry","Boolean","slice","authors","length","authorsRef","snapshots","getAll","snapshot","exists","author","error","console","ENTRY_SCHEDULE_SKIPPED","scheduleAlreadyRefused","isDueScheduled","Number","POSITIVE_INFINITY","Date","now","isPendingScheduled","scheduledPublishingPermission","org","isLive","permission","flipDueEntry","docRef","update","scheduleStatus","catch","status","publishedAt","LIVE_ENTRY_FIELDS","SCHEDULED_SOURCE_MAX","liveEntriesBase","entriesRef","select","readLiveEntryDocs","dated","orderBy","scheduled","reachedBound","undated","entryDoc","seen","has","add","push","unordered","toLiveEntries","ref","title","sort","a","b","listLiveEntries","due","pendingSchedule","some","readCollectionListingPage","options","cursorId","before","after","cursorDoc","backwards","startAfter","hasMore","pageDocs","reverse","resolveCollectionPageCursor","skip","page","perPage","isFinite","offset","getPublishedCollectionSource","key","revalidate","read","readPublishedCollectionSource","store","getPublishedCollectionEntries","applyCategoryAndPagination","listing","routedCategory","categorySlug","match","known","windowStart","windowed","pageEnd","last","first","cursorHasMore","countable","totalEntries","pagination","nextCursor","prevCursor","totalPages","getCollectionContent","entrySlug","preview","previewUnpublishedEntry","entryQuery","source","entriesReachedBound","cursor","cursored","dueHere","entryPreview","String","publishAtSeconds","body"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-collection-content.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 {\n AUTHORS_MAX_PER_HOST,\n type CollectionCategory,\n collectionCategorySlug,\n checkEntitlement,\n COLLECTION_SOURCE_MAX,\n collectionTotalPages,\n type ContentAuthorRecord,\n entryMatchesCategoryRoute,\n hostCollectionKind,\n normalizeContentAuthor,\n normalizeContentSchemaType,\n resolveCollectionCategoryBySlug,\n resolveEntryAuthor,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * ONE cached read per collection, shared by every surface that lists it\n * (AGL-1302).\n *\n * It began as the compose-time source only: a Collection entries block in a\n * shared layout re-read up to ~100 entry docs on EVERY page of the site. The\n * routed listing was left out on the argument that the page's own ISR entry\n * amortized it — which is true of ONE address and false of a collection,\n * because a collection is not one address. `/blog`, `/blog/page/2…10`, a\n * `/blog/category/{slug}` per category and `/blog/rss.xml` are all the same\n * data, each paying its own `1 + entries + authors` per window, beside a\n * cache already holding exactly that.\n *\n * The other half of that argument was real and is answered rather than\n * dropped: `flipDueEntry` is a write, and nothing else publishes a content\n * entry, so a cache that stored a collection with a schedule still pending\n * would suppress the render that publishes it. `getPublishedCollectionSource`\n * therefore declines to STORE exactly those collections — see its `store`\n * predicate — which leaves scheduled publishing on the render window it has\n * always been on, and puts everything else on this TTL.\n */\nconst COLLECTION_SOURCE_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * Resolve a public content-collection slug (AGL-954). Commerce's product\n * collections share `hosts/{hostId}/collections`, and a slug is only unique\n * within a kind — a bare `limit(1)` handed the URL to whichever doc Firestore\n * returned first, so a catalog collection could shadow a blog. Reads a small\n * window instead and takes the first content-kind match.\n */\nasync function findContentCollection(\n hostId: string,\n collectionSlug: string,\n): Promise<FirebaseFirestore.QueryDocumentSnapshot | undefined> {\n const matches = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .where('slug', '==', collectionSlug)\n .limit(5)\n .get()\n return matches.docs.find(\n (docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'content',\n )\n}\n\nexport interface CollectionEntrySummary {\n $id: string\n title: string\n slug: string\n excerpt?: string\n /**\n * The byline TEXT (AGL-686). Either the entry's own legacy free-typed\n * string or — since AGL-2486 — the name of the author record `authorId`\n * points at, resolved here so every downstream reader (the Entry Meta\n * block, `{{entry.author}}`, the RSS feed) keeps asking one field.\n */\n authorName?: string\n /** Reference into `hosts/{hostId}/authors` (AGL-2486). */\n authorId?: string\n /**\n * The resolved author RECORD (AGL-2486) — what `Article.author` is built\n * from. Null when the entry names no author, in which case the page falls\n * back to the site's publisher entity exactly as it always has.\n */\n author?: ContentAuthorRecord | null\n body?: string\n coverImage?: string\n /** `og:image:alt` for the cover (AGL-2417); travels WITH `coverImage`. */\n coverImageAlt?: string\n /**\n * The featured video (AGL-2956): a media reference or a URL, a Wistia link\n * included, in the shape `coverImage` is. See\n * `CollectionEntryRecord.coverVideo`.\n */\n coverVideo?: string\n /** Search-result title override (AGL-582); falls back to `title`. */\n seoTitle?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /**\n * Stable reference into the collection's `categories` taxonomy\n * (AGL-582); resolved to a display name at render.\n */\n categoryId?: string\n /** Legacy free-typed bucket (AGL-582); read-only fallback. */\n category?: string\n /** Free-form labels (AGL-582). */\n tags?: string[]\n publishedAt?: { seconds: number } | null\n /**\n * Last edited, which is what `Article.dateModified` publishes (AGL-2534).\n *\n * Distinct from {@link publishedAt} on purpose: re-dating a post is not\n * editing it, so the console writes `publishedAt` alone when an author\n * backdates and this stays put. Google reads `dateModified` for freshness.\n */\n updatedAt?: { seconds: number } | null\n /**\n * The collection this entry came out of (AGL-2518), stamped only by a\n * reader that MIXES collections — the author page. Unset on every routed\n * listing, where the route already answers the question. See\n * `CollectionEntryRecord.collectionSlug`.\n */\n collectionSlug?: string\n /** The display name of {@link collectionSlug}. */\n collectionName?: string\n}\n\n/** Entry-doc fields shared by the list and single-entry mappers (AGL-582). */\nfunction mapEntryFields(\n value: FirebaseFirestore.DocumentData,\n): Pick<\n CollectionEntrySummary,\n | 'excerpt'\n | 'coverImage'\n | 'coverImageAlt'\n | 'coverVideo'\n | 'seoTitle'\n | 'seoDescription'\n | 'authorName'\n | 'authorId'\n | 'categoryId'\n | 'category'\n | 'tags'\n | 'updatedAt'\n> {\n return {\n excerpt: value['excerpt'] ?? '',\n // The byline was DECLARED on `CollectionEntrySummary` (AGL-686) and\n // mapped by nobody, so `entry.authorName` was `undefined` on every entry\n // this loader returned — which is every routed entry page and every\n // Collection entries block. The console collected the field, the rules\n // stored it, the JSON-LD builder read it and the Entry Meta block printed\n // it, and all three saw nothing, because the one hop between Firestore\n // and them dropped it (AGL-2486). Written but never read, in the\n // direction that leaves no error behind.\n authorName: value['authorName'] ?? '',\n authorId: value['authorId'] ?? '',\n coverImage: value['coverImage'] ?? '',\n coverImageAlt: value['coverImageAlt'] ?? '',\n // Here, where both read paths pick it up, so a list card and the routed\n // entry page can each bind the featured video (AGL-2956).\n coverVideo: value['coverVideo'] ?? '',\n seoTitle: value['seoTitle'] ?? '',\n seoDescription: value['seoDescription'] ?? '',\n categoryId: value['categoryId'] ?? '',\n category: value['category'] ?? '',\n tags: Array.isArray(value['tags'])\n ? value['tags'].filter((tag): tag is string => typeof tag === 'string')\n : [],\n /*\n `dateModified`'s source (AGL-2534), and it was missing for the same\n reason `authorName` was — the reason this function's own comment above\n describes.\n\n The console has written `updatedAt` on every save, `page.tsx` reads\n `entry.updatedAt.seconds` to publish `Article.dateModified`, a spec\n asserts the conversion, and two console comments explain why it must not\n track `publishedAt`. Nothing mapped it, so `entry.updatedAt` was\n `undefined` on every entry the loader has ever returned and no published\n article has ever carried a `dateModified` — the freshness signal Google\n reads. The spec passed throughout because it builds its entry object by\n hand and never crosses this boundary.\n\n Mapped HERE rather than beside `publishedAt` at the two call sites: this\n is an ordinary field with no `publishAt` fallback and nothing sorts on\n it, so one place is enough — and one place is what stops the next read\n path from forgetting it.\n */\n updatedAt: value['updatedAt']?.seconds\n ? { seconds: value['updatedAt'].seconds }\n : null,\n }\n}\n\n/**\n * The collection doc's category taxonomy (AGL-582), sanitized: only\n * `{ id, name }` pairs with non-empty strings survive, order preserved.\n *\n * `description` rides along when the author wrote one and is dropped when it\n * is blank or not a string, so the head can tell \"described\" from \"not\n * described\" by truthiness alone — an empty string reaching the metadata\n * would suppress the template screen's description and leave the listing with\n * none at all.\n */\nfunction mapCollectionCategories(value: unknown): CollectionCategory[] {\n if (!Array.isArray(value)) return []\n return value\n .filter(\n (item): item is CollectionCategory =>\n typeof item?.id === 'string' &&\n item.id.trim() !== '' &&\n typeof item?.name === 'string' &&\n item.name.trim() !== '',\n )\n .map((item) => {\n const description =\n typeof item.description === 'string' ? item.description.trim() : ''\n return {\n id: item.id,\n name: item.name,\n ...(description ? { description } : {}),\n }\n })\n}\n\n/**\n * The routed view of a collection DOCUMENT (AGL-551): its name and the\n * template screens its list and entry routes render through.\n *\n * `slug` is the slug that was ASKED FOR rather than the one stored, which is\n * what every caller of this file has always returned — a listing has to build\n * its own URLs out of the segment the reader is standing on.\n */\nfunction mapCollectionDoc(\n collectionDoc: FirebaseFirestore.QueryDocumentSnapshot,\n collectionSlug: string,\n): CollectionContent['collection'] {\n return {\n $id: collectionDoc.id,\n displayName: collectionDoc.get('displayName') ?? collectionSlug,\n slug: collectionSlug,\n templateScreenId: collectionDoc.get('templateScreenId') ?? undefined,\n listScreenId: collectionDoc.get('listScreenId') ?? undefined,\n entryScreenId: collectionDoc.get('entryScreenId') ?? undefined,\n // Normalized HERE rather than at the head (AGL-2536), so an unrecognised\n // stored value can never reach the JSON-LD: an `@type` the vocabulary\n // does not define makes a consumer discard the whole node, costing the\n // page every property it publishes rather than just this one.\n schemaType: normalizeContentSchemaType(collectionDoc.get('schemaType')),\n categories: mapCollectionCategories(collectionDoc.get('categories')),\n }\n}\n\n/**\n * Resolve the author RECORDS a set of entries reference (AGL-2486).\n *\n * Costs ZERO reads when no entry names an `authorId`, which is every site\n * that has not adopted custom authors and every entry written before them —\n * the check is on the ids already in hand, not a probe of the collection. When\n * ids are present it is one `getAll` of the DISTINCT ones, bounded by\n * {@link AUTHORS_MAX_PER_HOST} and by the ≤100-entry page above it, rather\n * than a read per entry.\n *\n * Fail-open, like every other read in this file: an authors read that throws\n * leaves the entries with their legacy `authorName` (or the site entity) and\n * the page renders. A byline is not worth a 500.\n */\nasync function attachEntryAuthors(\n hostId: string,\n entries: CollectionEntrySummary[],\n): Promise<void> {\n const ids = [\n ...new Set(\n entries\n .map((entry) => (entry.authorId ?? '').trim())\n .filter(Boolean),\n ),\n ].slice(0, AUTHORS_MAX_PER_HOST)\n let authors: ContentAuthorRecord[] = []\n if (ids.length) {\n try {\n const authorsRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('authors')\n const snapshots = await firebaseAdmin\n .app()\n .firestore()\n .getAll(...ids.map((id) => authorsRef.doc(id)))\n authors = snapshots\n .map((snapshot) =>\n snapshot.exists\n ? normalizeContentAuthor(snapshot.data(), snapshot.id)\n : null,\n )\n .filter((author): author is ContentAuthorRecord => Boolean(author))\n } catch (error) {\n console.error(error)\n }\n }\n for (const entry of entries) {\n const author = resolveEntryAuthor(entry, authors)\n entry.author = author\n // The byline TEXT is denormalized onto the field everything downstream\n // already reads, so a record-backed author needs no change in the Entry\n // Meta block, the token map, the fallback nodes or the RSS feed. A record\n // WINS over the legacy string on the same entry: picking an author is the\n // more recent statement of who wrote it.\n if (author?.name) entry.authorName = author.name\n }\n}\n\n/**\n * Terminal refusal marker for an entry schedule, the flat-status twin of\n * `publishSchedule.status: 'skipped-unentitled'` on screens (AGL-1185).\n *\n * A field rather than a new `status` value on purpose. `status` is queried\n * (`where('status', 'in', [...])`), rendered by the console, and sorted on by\n * `bundle-timestamps.ts`; adding a member would have meant auditing every one\n * of those readers. A sibling field is invisible to all of them and is read\n * only here.\n */\nconst ENTRY_SCHEDULE_SKIPPED = 'skipped-unentitled'\n\n/** A schedule this entry already had declined, and will not have reconsidered. */\nfunction scheduleAlreadyRefused(\n value: FirebaseFirestore.DocumentData,\n): boolean {\n return value['scheduleStatus'] === ENTRY_SCHEDULE_SKIPPED\n}\n\n/** A scheduled entry whose time has come — before any plan question. */\nexport function isDueScheduled(value: FirebaseFirestore.DocumentData): boolean {\n return (\n value['status'] === 'scheduled' &&\n !scheduleAlreadyRefused(value) &&\n (value['publishAt']?.seconds ?? Number.POSITIVE_INFINITY) * 1000 <=\n Date.now()\n )\n}\n\n/**\n * A scheduled entry still waiting on its time: not due yet, and not refused.\n * Nothing but a render publishes a content entry, so a cache that stored a\n * read holding one would withhold the render that notices it come due — see\n * {@link LiveEntriesRead.pendingSchedule}.\n */\nexport function isPendingScheduled(\n value: FirebaseFirestore.DocumentData,\n): boolean {\n return (\n value['status'] === 'scheduled' &&\n !scheduleAlreadyRefused(value) &&\n !isDueScheduled(value)\n )\n}\n\n/**\n * Is this host's plan allowed to publish on a schedule? (AGL-471 shape.)\n *\n * `React.cache`-deduped per request via `getOrgForHost`, and — this is the\n * part that keeps it off the hot path — every caller below only asks once it\n * has already found a due scheduled entry. A collection with nothing due pays\n * nothing, which is almost every render.\n *\n * THREE answers, not two, and the third is the point. `refused` means we read\n * the plan and it does not carry the entitlement. `unresolved` means we could\n * not find out. Both withhold the entry, but only `refused` may write the\n * terminal marker — burning a schedule permanently on the strength of a\n * hostIndex miss or a transient rejection would destroy a customer's post for\n * a reason that may not be true a second later.\n *\n * Withholding on `unresolved` rather than publishing is what every other\n * entitlement caller on the tenant runtime already does: `apply-publish-schedule`\n * here, and the automation engine's two runners, all pass a possibly\n * undefined org straight into `checkEntitlement`, which resolves a missing\n * plan as free and denies (AGL-247). Opening here instead would make this the\n * one gate in the lib that admits when it cannot see — the exact shape of the\n * free-tier leak `no-plan-gated-entitlement` exists to forbid.\n *\n * The blast radius of withholding is deliberately small: `isLive` answers true\n * for `status: 'published'` before it ever consults this, so an unresolved\n * read hides only the due-scheduled entry, never the published ones, and the\n * next render retries.\n */\nexport type SchedulePermission = 'allowed' | 'refused' | 'unresolved'\n\nexport async function scheduledPublishingPermission(\n hostId: string,\n): Promise<SchedulePermission> {\n try {\n const org = (await getOrgForHost(hostId))?.org\n if (!org) return 'unresolved'\n return checkEntitlement(org, 'scheduledPublishing') ? 'allowed' : 'refused'\n } catch (error) {\n // Caught rather than thrown: the only caller sits inside\n // `getCollectionContent`'s try/catch, which returns an EMPTY collection —\n // so an unhandled rejection here would blank every published entry on the\n // page, not just the scheduled one.\n console.error(error)\n return 'unresolved'\n }\n}\n\n/**\n * Scheduled entries (AGL-123) go live lazily like AGL-61: a due\n * `publishAt` counts as published for this render, and the doc is flipped\n * to `published` fail-open so the state becomes durable.\n *\n * PLAN GATE (AGL-471). `scheduledPublishing` is a Business entitlement, and\n * until now nothing on the entry path checked it: the console let any plan\n * write `status: 'scheduled'`, and this render path published it. Scheduling\n * worked end to end on Free. The screens path has gated this since AGL-471\n * and records its refusal since AGL-1185 — entries were simply never wired\n * to either, which is why the leak was invisible from the screens side.\n *\n * The permission is threaded in rather than resolved here so the org read\n * happens once per call site instead of once per entry.\n *\n * Exported for the one other reader that decides whether an entry is on the\n * site — whether a LINK to it resolves (AGL-3118) — so a link and the page it\n * points at can never disagree about which entries exist.\n */\nexport function isLive(\n value: FirebaseFirestore.DocumentData,\n permission: SchedulePermission,\n): boolean {\n if (value['status'] === 'published') return true\n return permission === 'allowed' && isDueScheduled(value)\n}\n\n/**\n * Make the due state durable — or record that it was refused.\n *\n * The refusal is TERMINAL, for the AGL-1185 reason: left as a bare pending\n * `scheduled`, the entry stays permanently due, so the day the org upgrades\n * to Business the next render publishes it. Content scheduled on a plan that\n * could not honour it, and forgotten, surfacing during an upgrade — exactly\n * when nobody is looking for it. Recording the refusal is what makes it stop\n * being due, and it also stops this path re-reading the org on every\n * subsequent render.\n *\n * Both writes fail open: an error leaves today's state, and the next render\n * retries.\n */\nfunction flipDueEntry(\n docRef: FirebaseFirestore.DocumentReference,\n value: FirebaseFirestore.DocumentData,\n permission: SchedulePermission,\n): void {\n if (value['status'] !== 'scheduled') return\n if (scheduleAlreadyRefused(value)) return\n // `unresolved` writes NOTHING. It withholds this render and leaves the\n // schedule exactly as it found it, so a later render can still publish it.\n if (permission === 'unresolved') return\n if (permission === 'refused') {\n if (!isDueScheduled(value)) return\n docRef\n .update({ scheduleStatus: ENTRY_SCHEDULE_SKIPPED })\n .catch((error) => console.error(error))\n return\n }\n docRef\n .update({ status: 'published', publishedAt: value['publishAt'] })\n .catch((error) => console.error(error))\n}\n\nexport interface CollectionContent {\n /**\n * The zone this site's dates read in (AGL-3237), carried on the CONTENT so\n * it reaches the client.\n *\n * `collection-fallback.tsx` renders through `next/dynamic` from\n * `catch-all-client`, so it formats dates in the browser as well as on the\n * server. A zone it read from its own runtime would be the visitor's, which\n * is precisely the hydration mismatch AGL-1926 fixed — so the server\n * decides it once and it travels here, in the props both renders read.\n */\n timeZone?: string\n collection: {\n $id: string\n displayName: string\n slug: string\n /**\n * Legacy entry-template screen (AGL-105); superseded by\n * `entryScreenId` but still honored when only it is set.\n */\n templateScreenId?: string\n /** List-template screen (AGL-551); `/{collection}` renders through it. */\n listScreenId?: string\n /**\n * Entry-template screen (AGL-551); `/{collection}/{entry}` renders\n * through it with `{{entry.*}}` tokens.\n */\n entryScreenId?: string\n /**\n * What KIND of article this collection publishes (AGL-2536) — the\n * `schema.org` type its entries serialise as. Unset publishes `Article`,\n * which is what every collection published before the setting existed.\n */\n schemaType?: string\n /**\n * Category taxonomy (AGL-582): entries reference these by stable\n * `id`; `name` is the renameable display label.\n */\n categories?: CollectionCategory[]\n } | null\n entries: CollectionEntrySummary[]\n entry: CollectionEntrySummary | null\n /**\n * Whether the read that produced `entries` stopped at\n * {@link COLLECTION_SOURCE_MAX} (AGL-1516). Set on LIST routes only —\n * an entry route reads one document by slug and bounds nothing.\n */\n entriesReachedBound?: boolean\n /**\n * Present ONLY when a verified preview grant revealed an entry the public\n * site withholds (AGL-3205) — never on a public render, and never for an\n * entry that is already live. Its absence is what the preview chrome reads\n * to say \"this one is actually published\", so it must not be set\n * defensively.\n */\n entryPreview?: {\n /** The stored `status` — `scheduled` or `draft`. */\n status: string\n /** The instant it is scheduled for, or null when nothing is scheduled. */\n publishAtSeconds: number | null\n }\n /** List pagination (AGL-620); null for entry pages or unpaginated lists. */\n pagination?: CollectionPagination | null\n /**\n * The category this listing is filtered to (AGL-1321); null on the\n * canonical unfiltered list and on entry pages.\n */\n category?: CollectionRouteCategory | null\n error: unknown\n}\n\n/** The category a `/{collection}/category/{slug}` route addresses (AGL-1321). */\nexport interface CollectionRouteCategory {\n /** The URL segment, normalized — what the canonical link must say. */\n slug: string\n /** Taxonomy id; absent when the segment matched no known category. */\n id?: string\n /** Display label; falls back to the raw segment for an unknown category. */\n name: string\n /**\n * The taxonomy's {@link CollectionCategory.description}, carried onto the\n * route so the head can describe the FILTERED listing rather than inherit\n * the whole collection's description. Absent for an unknown segment, which\n * names no category and therefore has nothing to describe.\n */\n description?: string\n /**\n * Whether the segment resolved against the collection's taxonomy. An\n * unknown category still renders — an empty listing, not a crash — but the\n * page must not invite indexing of a URL that names nothing.\n */\n known: boolean\n}\n\nexport interface CollectionPagination {\n /**\n * 1-based counter for display only (AGL-3219).\n *\n * It is what `{{pagination.page}}` renders and nothing reads it back: a\n * cursor page is addressed by the document it starts after, so this number\n * describes how far a reader has walked rather than where the read began.\n * Hand-edit it in a URL and the label is wrong; the entries are not.\n */\n page: number\n perPage: number\n /**\n * The document id the NEXT (older) page starts after, or `''` when this is\n * the last page (AGL-3219).\n *\n * Set from a `limit(perPage + 1)` probe: the extra row is the only evidence\n * needed that an older page exists, and it costs one document rather than\n * the `count()` aggregation a total used to need. It is also the only\n * honest answer available — see {@link CollectionPagination.totalPages}.\n */\n nextCursor: string\n /** The document id the PREVIOUS (newer) page starts before, or `''`. */\n prevCursor: string\n /**\n * How many pages there are.\n *\n * @deprecated AGL-3219. Still resolved, so a template binding\n * `{{pagination.totalPages}}` keeps rendering, but no longer a promise: a\n * total can only be stated for a collection that fits inside one read, and\n * past that it is absent rather than wrong. It used to be derived from a\n * `count()` over a set the listing's two reads disagreed about, which is\n * how the page 10/11 seam came to repeat an entry. Bind\n * {@link CollectionPagination.nextCursor} instead — `''` means there is\n * nothing older, which is the question a pager is actually asking.\n */\n totalPages?: number\n /** @deprecated AGL-3219, with {@link CollectionPagination.totalPages}. */\n totalEntries?: number\n /**\n * Where `entries` begins in the collection's own order (AGL-3213): 0 for a\n * listing served from the cached read, and the page's own offset for one\n * served by a window read past that read's bound.\n *\n * Both windowing sites subtract it — `collectionEntriesPageWindow` on the\n * way into props, and the Collection entries block on the way into compose.\n * Without it a windowed listing renders empty, because every one of them\n * slices `[(page - 1) * perPage, …)` on the premise that `entries` starts at\n * the beginning of the collection.\n */\n windowStart?: number\n}\n\n/**\n * A bounded read of a collection's live entries (AGL-1516).\n *\n * `reachedBound` is a fact about the QUERY, not about `entries`, and the two\n * genuinely differ: the query asks for `status in ['published', 'scheduled']`\n * and the filter below then drops everything not live yet, so a read that came\n * back holding all {@link COLLECTION_SOURCE_MAX} docs can hand back fewer.\n * Counting the survivors — which is all a downstream consumer can do — reads\n * that as a complete collection, and the one thing a truncated read must never\n * be allowed to claim is completeness.\n */\ninterface LiveEntriesRead {\n entries: CollectionEntrySummary[]\n /** The query came back holding its own `.limit()`. */\n reachedBound: boolean\n /**\n * The read saw a `scheduled` entry whose `publishAt` has NOT arrived and\n * which has not been terminally refused — a schedule this collection is\n * still waiting on.\n *\n * Nothing promotes a content entry on a beat: `publish-schedule-job.ts` is\n * screens-only, so `isLive`/`flipDueEntry` running during a render is the\n * entire mechanism. A cached source therefore does not merely serve stale\n * entries, it withholds the render that would have published one, for as\n * long as the entry stays cached. This is what lets the cache decline to\n * store exactly those collections, so a schedule keeps landing on the\n * render window rather than on the TTL.\n */\n pendingSchedule: boolean\n}\n\n/**\n * The fields a LISTING read of an entry needs — the field mask on the query\n * below (AGL-3213).\n *\n * The point of the mask is the field that is NOT in it. `body` is the whole\n * post, and `mapEntryFields` has never mapped it on this path: a list card\n * binds a title, an excerpt, a cover and a byline, and the routed entry page\n * reads its one document separately. So the markdown of up to\n * {@link COLLECTION_SOURCE_MAX} posts crossed the wire, was parsed out of the\n * response, and was dropped one function later — on every fill of a cache\n * that every listing address, every \"Latest posts\" rail, the feed and the\n * author page share. A changelog is the worst case and also the common one.\n *\n * Firestore bills the document read either way, so this buys no reads; it\n * buys egress and the JSON parse, which is the part of a collection render\n * that grows with how much people have written.\n *\n * Site search is unaffected and must stay that way: it matches on `body`\n * through its OWN query in `apps/tenant/utils/search-content.ts`, which this\n * mask does not touch.\n *\n * ⛔ A reader added to `mapEntryFields` or to the liveness/schedule helpers\n * must be added HERE in the same edit. A field left out does not error — it\n * arrives `undefined`, which is exactly how `authorName` and `updatedAt` went\n * missing for months (AGL-2486, AGL-2534). The four schedule fields are\n * listed first for that reason: `status`, `publishAt` and `scheduleStatus`\n * decide whether an entry is live at all, and `flipDueEntry` WRITES\n * `publishAt` back as `publishedAt`, so a mask that dropped it would publish\n * a due entry with no date.\n */\nconst LIVE_ENTRY_FIELDS = [\n 'status',\n 'publishAt',\n 'publishedAt',\n 'scheduleStatus',\n 'title',\n 'slug',\n 'excerpt',\n 'authorName',\n 'authorId',\n 'coverImage',\n 'coverImageAlt',\n 'coverVideo',\n 'seoTitle',\n 'seoDescription',\n 'categoryId',\n 'category',\n 'tags',\n 'updatedAt',\n] as const\n\n/**\n * Most SCHEDULED entries one live read considers (AGL-3213).\n *\n * Read by its own query rather than taken from the dated page below, because\n * a schedule is invisible to that page: scheduling writes `publishAt` and\n * never `publishedAt`, and `flipDueEntry` during a render is the only thing\n * that publishes one. A schedule the read misses is a post that never goes\n * out. The set is small by nature — a hundred pending schedules on one\n * collection is already an unusual editorial calendar.\n */\nconst SCHEDULED_SOURCE_MAX = 100\n\n/** Everything a public listing may show, before any ordering. */\nfunction liveEntriesBase(\n entriesRef: FirebaseFirestore.CollectionReference,\n): FirebaseFirestore.Query {\n return (\n entriesRef\n .where('status', 'in', ['published', 'scheduled'])\n // Everything this path reads, and nothing else (AGL-3213) — see\n // {@link LIVE_ENTRY_FIELDS}.\n .select(...LIVE_ENTRY_FIELDS)\n )\n}\n\n/**\n * The documents a live read considers, in the order the site shows them\n * (AGL-3213).\n *\n * ## Why this is ordered now, and why ordering alone would have broken it\n *\n * It was `where(status).limit(100)` with NO `orderBy`, sorted in memory\n * afterwards. That is not the newest hundred — it is a hundred documents in\n * NAME order, then sorted. Under the bound the two agree, because a hundred\n * out of a hundred is everything; past it they diverge completely. This site's\n * own changelog reached 166 live entries and its listing showed an arbitrary\n * hundred of them, chosen by document id, with 66 releases reachable only at\n * their own URLs.\n *\n * The comment that used to sit here was right about `orderBy`, though:\n * Firestore returns only documents that HAVE the ordered field, so a dated\n * read does not mis-sort an entry without a date — it hides it. Two of those\n * exist and both matter:\n *\n * A SCHEDULE carries `publishAt` and no `publishedAt` until it goes out.\n * Ordering alone would have stopped scheduled posts publishing\n * at all, silently, because nothing else publishes them.\n * AN IMPORT restores whatever the bundle carried, and\n * `/api/hosts/resources` validates no field for presence\n * either — so a published entry with no `publishedAt` exists.\n *\n * So it is three queries, in the shape the console's sorted window uses\n * (AGL-2853): the DATED page, every SCHEDULE, and — only when the dated page\n * came back SHORT, which is the server's own proof that the collection fits\n * inside the bound — a scan for live entries carrying no date. Past the bound\n * an undated entry sorts after every dated one by definition, so it belongs to\n * the tail pages, which are served by their own window read.\n */\nasync function readLiveEntryDocs(\n entriesRef: FirebaseFirestore.CollectionReference,\n): Promise<{\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n reachedBound: boolean\n}> {\n try {\n const dated = await liveEntriesBase(entriesRef)\n .orderBy('publishedAt', 'desc')\n // The document name breaks ties, so two entries published in the same\n // second cannot swap places between two reads and move a page boundary\n // under a reader. Descending to match the date: a composite index ends\n // with `__name__` in the last field's direction, which makes this the\n // `(status, publishedAt DESC)` index the console's table already needs.\n .orderBy('__name__', 'desc')\n // Named rather than literal (AGL-1516): a search index has to be able to\n // say \"this read reached its bound\", and it can only do that against a\n // bound it shares with the query. `collectionSourceReachedBound` reads\n // the same constant.\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n\n const scheduled = await entriesRef\n .where('status', '==', 'scheduled')\n .select(...LIVE_ENTRY_FIELDS)\n .limit(SCHEDULED_SOURCE_MAX)\n .get()\n\n /*\n * EITHER read stopping at its own limit means entries went unseen.\n *\n * The dated page is the usual one. The schedule page is the case a dated\n * read alone cannot even detect: a collection holding nothing but pending\n * schedules returns ZERO dated documents, so a bound measured only there\n * would report a complete read of an empty collection — and \"nothing is\n * live here\" is the claim that takes a listing off the site (AGL-3101).\n */\n const reachedBound =\n dated.docs.length >= COLLECTION_SOURCE_MAX ||\n scheduled.docs.length >= SCHEDULED_SOURCE_MAX\n\n const undated = reachedBound\n ? []\n : (\n await liveEntriesBase(entriesRef)\n .orderBy('__name__')\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n ).docs.filter((entryDoc) => !entryDoc.get('publishedAt'))\n\n const seen = new Set<string>()\n const docs: FirebaseFirestore.QueryDocumentSnapshot[] = []\n for (const entryDoc of [...dated.docs, ...scheduled.docs, ...undated]) {\n if (seen.has(entryDoc.id)) continue\n seen.add(entryDoc.id)\n docs.push(entryDoc)\n }\n return { docs, reachedBound }\n } catch (error) {\n /*\n * FAIL SOFT TO THE UNORDERED READ.\n *\n * The ordered query needs the `(status, publishedAt DESC)` composite\n * index. Indexes do NOT ship with a promotion — RELEASING.md deploys them\n * by hand afterwards — so the window between the code landing and the\n * index existing has to degrade rather than break. An arbitrary hundred\n * is a bad listing; a 500 is a customer's blog down.\n */\n console.error(error)\n const unordered = await liveEntriesBase(entriesRef)\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n return {\n docs: unordered.docs,\n reachedBound: unordered.docs.length >= COLLECTION_SOURCE_MAX,\n }\n }\n}\n\n/** The live entries among `docs`, newest first, publishing any that came due. */\nfunction toLiveEntries(\n docs: readonly FirebaseFirestore.QueryDocumentSnapshot[],\n permission: SchedulePermission,\n): CollectionEntrySummary[] {\n return (\n docs\n .filter((entryDoc) => isLive(entryDoc.data(), permission))\n .map((entryDoc) => {\n const value = entryDoc.data()\n flipDueEntry(entryDoc.ref, value, permission)\n return {\n $id: entryDoc.id,\n title: value['title'] ?? entryDoc.id,\n slug: value['slug'] ?? entryDoc.id,\n ...mapEntryFields(value),\n publishedAt: (value['publishedAt'] ?? value['publishAt'])\n ? {\n seconds: (value['publishedAt'] ?? value['publishAt']).seconds,\n }\n : null,\n }\n })\n // An entry carrying no date at all sorts last rather than to 1970 —\n // the same place the query's own order puts it.\n .sort(\n (a, b) => (b.publishedAt?.seconds ?? 0) - (a.publishedAt?.seconds ?? 0),\n )\n )\n}\n\n/**\n * Fetches a collection's live entries (newest first), shared by the route\n * loader and the compose-time Collection entries block (AGL-551).\n */\nasync function listLiveEntries(\n entriesRef: FirebaseFirestore.CollectionReference,\n hostId: string,\n): Promise<LiveEntriesRead> {\n const { docs, reachedBound } = await readLiveEntryDocs(entriesRef)\n\n // Ask the plan question ONLY if something is actually due (AGL-471). A\n // collection with no due schedule — almost every render — never reads the\n // org at all.\n const due = docs.filter((entryDoc) => isDueScheduled(entryDoc.data()))\n const permission: SchedulePermission = due.length\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n\n // Record the terminal refusal on its own pass, because a refused entry is\n // NOT live and so never reaches the `flipDueEntry` inside the map below.\n // Without this the entry stays due forever: excluded from every render, and\n // re-reading the org on each one.\n if (permission !== 'allowed') {\n for (const entryDoc of due) {\n flipDueEntry(entryDoc.ref, entryDoc.data(), permission)\n }\n }\n\n // Measured on the RAW docs, before the liveness filter (AGL-1516): a\n // not-yet-due entry is filtered out one line down, so this is the last\n // place that can see one at all.\n const pendingSchedule = docs.some((entryDoc) =>\n isPendingScheduled(entryDoc.data()),\n )\n\n const entries = toLiveEntries(docs, permission)\n\n return {\n entries,\n reachedBound,\n pendingSchedule,\n }\n}\n\n/** One page of a listing, and whether anything older follows it. */\ninterface CollectionListingPage {\n entries: CollectionEntrySummary[]\n /** The `limit(perPage + 1)` probe found an extra row. */\n hasMore: boolean\n}\n\n/**\n * One page of a listing that starts PAST the cached read (AGL-3213),\n * addressed by the document it continues from rather than by a position\n * (AGL-3219).\n *\n * Uncached on purpose, and it is the only read on the collection path that is.\n * The cached source exists because `/blog`, every listing address, the feed\n * and every \"Latest posts\" rail want the same first hundred entries\n * (AGL-1302); a page past that bound wants ten entries nobody else is asking\n * for, and its own ISR entry is already the cache for them.\n *\n * ## Why a cursor, and not the offset this replaces\n *\n * `.offset(n)` asks for a POSITION, and a listing takes its inserts at the\n * head, so every publish moves every position by one. The head pages and this\n * read are cached under different policies and revalidated by different\n * triggers — the publish fan-out refreshes the head eagerly and deliberately\n * stops there — so the two sides were routinely describing the collection as\n * it stood at two different moments, and the seam between them repeated an\n * entry or hid one for as long as the slower side lagged. `v1.0.0-beta.147`\n * shipped and `/changelog` showed `v1-0-0-beta-36` as both the last entry of\n * page 10 and the first of page 11.\n *\n * `startAfter(doc)` asks a question whose answer does not move: what follows\n * THIS entry. Publish a hundred entries at the head and this page returns the\n * same ten. The seam cannot drift because there is no longer a number on\n * either side of it to disagree about.\n *\n * `offset` also billed every document it skipped; a cursor bills none of them.\n */\nasync function readCollectionListingPage(options: {\n hostId: string\n collectionSlug: string\n /** Start after this document id — the older direction. */\n after?: string\n /** Start before this document id — the newer direction. */\n before?: string\n limit: number\n}): Promise<CollectionListingPage | null> {\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) return null\n const entriesRef = collectionDoc.ref.collection('entries')\n\n const cursorId = options.before || options.after\n if (!cursorId) return null\n // A direct get, not a `where('slug', ...)` query: the cursor IS the\n // document name, which is the second field the read orders on, so the\n // snapshot it needs is one document rather than an index lookup.\n const cursorDoc = await entriesRef.doc(cursorId).get()\n // A cursor naming a document that no longer exists — deleted, or a URL\n // someone kept — cannot be positioned against. Null sends the caller back\n // to the cached head, which is a page the reader can act on.\n if (!cursorDoc.exists) return null\n\n // Backwards is the same query read the other way up, so both directions\n // rest on an index the project already deploys: `(status, publishedAt)`\n // exists ascending and descending both.\n const backwards = Boolean(options.before)\n const snapshot = await liveEntriesBase(entriesRef)\n .orderBy('publishedAt', backwards ? 'asc' : 'desc')\n .orderBy('__name__', backwards ? 'asc' : 'desc')\n .startAfter(cursorDoc)\n // One more than the page: the extra row is the whole of \"is there\n // another page\", and it replaces a `count()` over the collection.\n .limit(options.limit + 1)\n .get()\n\n const hasMore = snapshot.docs.length > options.limit\n const pageDocs = snapshot.docs.slice(0, options.limit)\n // Read backwards, the newest of the page came back last.\n const docs = backwards ? [...pageDocs].reverse() : pageDocs\n\n const due = docs.filter((entryDoc) => isDueScheduled(entryDoc.data()))\n const permission: SchedulePermission = due.length\n ? await scheduledPublishingPermission(options.hostId)\n : 'allowed'\n if (permission !== 'allowed') {\n for (const entryDoc of due) {\n flipDueEntry(entryDoc.ref, entryDoc.data(), permission)\n }\n }\n const entries = toLiveEntries(docs, permission)\n // The byline reads `authorName`, which a record-backed author only has\n // once resolved — the cached source does this for the entries it holds,\n // and a windowed page holds entries it never saw (AGL-2486).\n await attachEntryAuthors(options.hostId, entries)\n return { entries, hasMore }\n } catch (error) {\n // Fail open to the cached head rather than to a 500: the caller keeps\n // whatever it already had, which is a page the reader has seen before\n // rather than an error they cannot get past.\n console.error(error)\n return null\n }\n}\n\n/**\n * The cursor a retired `/{collection}/page/{n}` address redirects onto\n * (AGL-3219).\n *\n * This is the ONE place a position is still resolved, and it is deliberately\n * the only one: a 301 is served once, the reader lands on an address that\n * cannot drift afterwards, and whatever the position meant at that instant is\n * the page they would have got anyway. Everything downstream is cursors.\n *\n * Keys only — `select()` with no fields asks Firestore for document names and\n * nothing else — so the skipped documents are billed at the cheapest rate the\n * offset can be had for. Returns `''` when the position is past the end,\n * which is a 404 rather than a redirect to nowhere.\n */\nexport async function resolveCollectionPageCursor(options: {\n hostId: string\n collectionSlug: string\n /** 1-based page whose PREVIOUS entry is the cursor. */\n page: number\n perPage: number\n}): Promise<string> {\n const skip = (options.page - 1) * options.perPage - 1\n if (!Number.isFinite(skip) || skip < 0) return ''\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) return ''\n const snapshot = await collectionDoc.ref\n .collection('entries')\n .where('status', 'in', ['published', 'scheduled'])\n .select()\n .orderBy('publishedAt', 'desc')\n .orderBy('__name__', 'desc')\n .offset(skip)\n .limit(1)\n .get()\n return snapshot.docs[0]?.id ?? ''\n } catch (error) {\n console.error(error)\n return ''\n }\n}\n\n/** Compose-time view of a collection: its live entries and its taxonomy. */\nexport interface PublishedCollectionSource {\n /**\n * The collection DOCUMENT this source was read from — its display name and\n * its template screen ids — or null when the slug names no content\n * collection.\n *\n * Carried so a routed listing can be served ENTIRELY from this cached\n * source. `getCollectionContent` used to resolve the same document itself\n * and then read the same entries again uncached, which meant `/blog`,\n * every `/blog/page/{n}`, every `/blog/category/{slug}` and the RSS feed\n * each paid a full collection read per regeneration while the identical\n * data already sat in this cache for every OTHER page on the site.\n */\n collection: CollectionContent['collection']\n entries: CollectionEntrySummary[]\n categories: CollectionCategory[]\n /**\n * Whether the read saw a schedule it is still waiting on — see\n * {@link LiveEntriesRead.pendingSchedule}. Never stored, only consulted by\n * the cache above, so no consumer has to know about it.\n */\n pendingSchedule?: boolean\n /**\n * Whether the entries read stopped at {@link COLLECTION_SOURCE_MAX}\n * (AGL-1516) — carried out of the loader because `entries.length` cannot\n * answer it once the liveness filter has run. Fail-open paths report\n * `false`: an empty result is not a bounded read, and describing it as one\n * would tell a reader their search covered less than it did.\n */\n reachedBound: boolean\n}\n\n/**\n * Published entries + category taxonomy for a collection resolved by slug —\n * the data source of the Collection entries block on arbitrary screens\n * (AGL-551/582). Fail-open: errors and unknown slugs resolve to an empty\n * list so a renamed collection never takes a published screen down.\n */\nexport async function getPublishedCollectionSource(options: {\n hostId: string\n collectionSlug: string\n}): Promise<PublishedCollectionSource> {\n try {\n return await withRenderCache({\n key: [\n 'tenant-collection-source',\n options.hostId,\n options.collectionSlug,\n ],\n revalidate: COLLECTION_SOURCE_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readPublishedCollectionSource(options),\n // A collection with a schedule still pending is SERVED but not STORED.\n //\n // No beat publishes a content entry — `flipDueEntry` during a render is\n // the whole mechanism — so storing this source would suppress the very\n // renders that would have noticed the entry coming due, and the post\n // would wait out the TTL rather than land at its time. Declining to\n // store leaves those collections exactly as uncached as they were\n // before this cache existed, which is the only cost this cache is not\n // allowed to reduce.\n //\n // Also refuses a collection that resolved to nothing, for the reason\n // `withRenderCache` states about negatives generally: a slug that\n // misses once must not miss for an hour.\n store: (value) => Boolean(value.collection) && !value.pendingSchedule,\n })\n } catch (error) {\n console.error(error)\n return readPublishedCollectionSource(options)\n }\n}\n\nasync function readPublishedCollectionSource(\n options: {\n hostId: string\n collectionSlug: string\n },\n): Promise<PublishedCollectionSource> {\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) {\n return {\n collection: null,\n entries: [],\n categories: [],\n reachedBound: false,\n }\n }\n const { entries, reachedBound, pendingSchedule } = await listLiveEntries(\n collectionDoc.ref.collection('entries'),\n options.hostId,\n )\n // The compose-time source feeds the Collection entries block, whose byline\n // reads `authorName` — so a record-backed author has to be resolved here\n // too, or the block prints nothing for the entries a list page shows\n // (AGL-2486). This result is the cached one, which is what keeps the extra\n // read amortized across every page of the site that carries the block.\n await attachEntryAuthors(options.hostId, entries)\n return {\n collection: mapCollectionDoc(collectionDoc, options.collectionSlug),\n entries,\n categories: mapCollectionCategories(collectionDoc.get('categories')),\n reachedBound,\n ...(pendingSchedule ? { pendingSchedule: true } : {}),\n }\n } catch (error) {\n console.error(error)\n return {\n collection: null,\n entries: [],\n categories: [],\n reachedBound: false,\n }\n }\n}\n\n/** Entries-only view of {@link getPublishedCollectionSource} (AGL-551). */\nexport async function getPublishedCollectionEntries(options: {\n hostId: string\n collectionSlug: string\n}): Promise<CollectionEntrySummary[]> {\n return (await getPublishedCollectionSource(options)).entries\n}\n\n/**\n * Narrows a listing to its routed category and stamps its pagination\n * (AGL-1321 / AGL-620).\n *\n * Category first, ALWAYS: the two have to describe the same set. Counting\n * pages over the whole collection and then filtering would advertise pages\n * that render empty and hide entries that exist.\n *\n * `entriesReachedBound` is read by the caller BEFORE this runs, because a\n * category route hands its already-narrowed entries to compose and the\n * entries block's \"measure the raw set, not the filtered one\" rule then has\n * nothing raw left to measure (AGL-1516).\n */\nfunction applyCategoryAndPagination(\n data: CollectionContent,\n options: {\n page?: number\n perPage?: number\n categorySlug?: string\n },\n listing?: {\n /** Where `data.entries` begins in that collection's order. */\n windowStart?: number\n /** The shared read came back holding its own limit. */\n reachedBound?: boolean\n /**\n * The `perPage + 1` probe of a CURSOR page found an extra row — set only\n * when this listing was served by one, because only that read can answer\n * it (AGL-3219).\n */\n cursorHasMore?: boolean\n },\n): void {\n const { page = 1, perPage } = options\n const routedCategory = (options.categorySlug ?? '').trim()\n if (routedCategory) {\n const match = resolveCollectionCategoryBySlug(\n data.collection?.categories,\n routedCategory,\n )\n data.category = {\n slug: collectionCategorySlug(routedCategory),\n ...(match ? { id: match.id } : {}),\n name: match?.name ?? routedCategory,\n ...(match?.description ? { description: match.description } : {}),\n known: Boolean(match),\n }\n data.entries = data.entries.filter((entry) =>\n entryMatchesCategoryRoute(\n entry,\n { slug: routedCategory, ...(match ? { category: match } : {}) },\n data.collection?.categories,\n ),\n )\n }\n\n if (perPage && perPage > 0) {\n /*\n * The total is the COLLECTION's, not the read's (AGL-3213).\n *\n * `entries.length` was the count, and on a collection inside the bound it\n * still is — the two are the same number there. Past the bound it is the\n * size of the window, which is how `/changelog` advertised \"Page 10 of\n * 10\" while holding 166 entries and linking to 100 of them.\n *\n * A ROUTED CATEGORY keeps counting its own entries, because that is the\n * only honest number available here: the narrowing happens in memory over\n * the cached head, so both the count and the listing describe the same\n * bounded set. Paging a category past the bound needs its own ordered\n * query and a `(categoryId, status, publishedAt DESC)` index; until then\n * a category of a very large collection is capped, and says so by\n * agreeing with what it shows.\n */\n const windowStart = Number(listing?.windowStart)\n const windowed = Number.isFinite(windowStart) && windowStart > 0\n\n /*\n * The cursors, which are the pager's real answer (AGL-3219).\n *\n * `data.entries` is either EXACTLY this page — a cursor read — or the\n * whole cached head, which every listing address shares and each slices\n * its own page out of. So the page's own last entry is at the end of the\n * array in the first case and at `page * perPage - 1` in the second, and\n * the cursor is that entry's document id.\n */\n const pageEnd = windowed ? data.entries.length : page * perPage\n const last = data.entries[pageEnd - 1]\n const first = data.entries[windowed ? 0 : (page - 1) * perPage]\n\n /*\n * Is there an older page?\n *\n * A cursor page KNOWS, because its read asked for one more entry than it\n * needed and the extra row came back. The head has to reason: either it\n * is holding more entries than this page shows, or it is holding all it\n * could read and the read stopped at its own bound, which is the one\n * thing `entries.length` can never tell you about the collection.\n */\n const hasMore =\n listing?.cursorHasMore ??\n (data.entries.length > pageEnd ||\n (Boolean(listing?.reachedBound) && !routedCategory))\n\n /*\n * The deprecated total, stated ONLY where it can be true.\n *\n * A collection that fits inside one read can be counted, and a template\n * binding `{{pagination.totalPages}}` keeps rendering the number it\n * always did. Past the bound there is no honest total — the count and\n * the listing were reading the collection at two different moments, which\n * is what made the seam repeat an entry — so it is left absent rather\n * than asserted. A routed category counts its own narrowed set, which is\n * bounded by the same head and therefore countable.\n */\n const countable = routedCategory || !listing?.reachedBound\n const totalEntries = countable ? data.entries.length : undefined\n\n data.pagination = {\n page,\n perPage,\n nextCursor: hasMore && last?.$id ? last.$id : '',\n prevCursor: page > 1 && first?.$id ? first.$id : '',\n ...(totalEntries === undefined\n ? {}\n : {\n totalEntries,\n totalPages: collectionTotalPages(totalEntries, perPage),\n }),\n ...(windowed ? { windowStart } : {}),\n }\n }\n}\n\n/**\n * Resolves a non-screen path against the host's content collections\n * (Content Collections & Blog): `/{collectionSlug}` returns the published\n * entry list, `/{collectionSlug}/{entrySlug}` one entry. Fail-open — errors\n * resolve to `collection: null` and the caller 404s.\n *\n * A listing resolves only for a collection with a live entry (AGL-3101); one\n * with nothing live answers `collection: null` as well, so its listing, feed\n * and markdown twin are not public until its first entry is.\n */\nexport async function getCollectionContent(options: {\n hostId: string\n collectionSlug: string\n entrySlug?: string\n /**\n * 1-based list page (AGL-620), now a DISPLAY counter (AGL-3219): it labels\n * the page the reader is on and no read is positioned from it. The entries\n * come from `after`/`before`, or from the head of the cached source when\n * neither is set.\n */\n page?: number\n /** Entries per page (AGL-620); when set the list is paginated. */\n perPage?: number\n /**\n * Continue AFTER this entry's document id — the older direction (AGL-3219).\n *\n * What `/{collection}?after={id}` carries. A page defined this way does not\n * move when something is published above it, which is the whole reason the\n * addresses stopped being positions.\n */\n after?: string\n /** Continue BEFORE this entry's document id — the newer direction. */\n before?: string\n /**\n * The site's zone (AGL-3237). Stamped onto the returned content so every\n * reader downstream — including the client fallback renderer — formats from\n * the same string rather than from its own runtime.\n */\n timeZone?: string\n /**\n * Category segment of `/{collection}/category/{slug}` (AGL-1321). Filters\n * the listing before pagination is computed, so page counts and the page\n * windows describe the FILTERED set rather than the whole collection.\n */\n categorySlug?: string\n /**\n * Reveal the named entry even though the public site withholds it — the\n * live-site preview of a scheduled post (AGL-3205).\n *\n * ⛔ A VERIFIED GRANT, NOT A REQUEST. The caller passes `true` only after a\n * signed preview token has been checked against the resolved hostId AND\n * this exact `collectionSlug`/`entrySlug` (`verifyCollectionPreviewToken`).\n * The token format stays in the app that receives the URL; what crosses into\n * this lib is the verdict, so nothing here can be tricked by a payload's own\n * spelling of which entry it names.\n *\n * ENTRY ROUTES ONLY, and one entry at a time. It is ignored on a list route\n * — a preview of one post must not add it to `/blog`, to the feed, or to a\n * Collection entries block on any other page, all of which read the SHARED\n * cached source. Nothing it touches is cached at all.\n *\n * The preview render also writes NOTHING: see the `flipDueEntry` guard\n * below. Publishing a schedule stays the sole business of a public render.\n */\n previewUnpublishedEntry?: boolean\n}): Promise<CollectionContent> {\n const { hostId, collectionSlug, entrySlug } = options\n // Never on a list route, whatever the caller passed: `entrySlug` is what\n // makes the grant addressable at all, and the list is the shared cached\n // read this must stay out of.\n const preview = Boolean(options.previewUnpublishedEntry) && Boolean(entrySlug)\n const data: CollectionContent = {\n // Stamped before any early return, so every shape this function can hand\n // back carries it — including the empty one a 404 renders from.\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n collection: null,\n entries: [],\n entry: null,\n pagination: null,\n category: null,\n error: null,\n }\n try {\n // A LIST route is served entirely from the CACHED source, which every\n // other page on the site already shares. It used to resolve the\n // collection and re-read its entries here, uncached, on every\n // regeneration of every listing address — `/blog`, nine `/blog/page/{n}`,\n // one `/blog/category/{slug}` per category, and the RSS feed — so a\n // collection of N live entries cost `1 + N + authors` reads per address\n // per window, for data byte-identical to what the cache was already\n // holding for the home page's \"Latest posts\" rail.\n //\n // An ENTRY route stays where it was: it reads ONE document by slug and\n // has nothing to share.\n if (!entrySlug) {\n const source = await getPublishedCollectionSource({\n hostId,\n collectionSlug,\n })\n if (!source.collection) return data\n // A collection is not a PAGE until something in it is live (AGL-3101).\n // Without this, creating one publishes `/{slug}` at once — an empty\n // listing with a feed and a markdown twin, before a word of it is\n // written. Answering it as no collection makes every listing address\n // 404 the way an unknown slug does, until the first entry is published\n // or its schedule comes due.\n //\n // Read off the UNFILTERED live set, before the category narrows it, so\n // an empty category of a live collection still renders. A read that\n // stopped at its bound cannot prove the collection empty — the live\n // entries may be past it — so that one keeps its listing. The rule\n // lives here and not in the source above: the source also feeds the\n // Collection entries block and the author page, and to them an empty\n // collection is an empty list, not a missing one.\n if (!source.entries.length && !source.reachedBound) return data\n data.collection = source.collection\n data.entries = source.entries\n data.entriesReachedBound = source.reachedBound\n /*\n * A page that starts past the cached read is served by its own window\n * (AGL-3213). Everything before that point comes out of the shared\n * source, so the pages a reader actually visits stay free.\n *\n * Only the unfiltered listing: a category route narrows in memory over\n * the same head, and a window read of the whole collection would hand\n * it ten entries of which any number may belong to another category.\n */\n const { page = 1, perPage } = options\n const cursor = (options.after ?? options.before ?? '').trim()\n let windowStart = 0\n let cursored: CollectionListingPage | null = null\n /*\n * A cursor address is served by its own read (AGL-3219). Everything\n * reachable without one comes out of the shared source, so the pages a\n * reader actually visits stay free.\n *\n * Only the unfiltered listing: a category route narrows in memory over\n * the same head, and a cursor read of the whole collection would hand it\n * ten entries of which any number belong to another category.\n */\n if (perPage && perPage > 0 && cursor && !(options.categorySlug ?? '').trim()) {\n cursored = await readCollectionListingPage({\n hostId,\n collectionSlug,\n ...(options.before ? { before: options.before } : { after: cursor }),\n limit: perPage,\n })\n // Null means the cursor read failed, or named a document that is\n // gone. The cached head is the better answer than an empty page: the\n // reader sees the newest entries rather than nothing at all.\n if (cursored) {\n data.entries = cursored.entries\n /*\n * `entries` is now EXACTLY this page, and both windowing sites\n * slice `[(page - 1) * perPage, …)`. Declaring the window to start\n * at that same offset makes their subtraction come out at zero, so\n * they take the page whole.\n *\n * Both sides of the subtraction are built from the same `page`, so\n * a hand-edited counter in a URL cancels itself out: the label is\n * wrong and the entries are still this cursor's page.\n */\n windowStart = (page - 1) * perPage\n }\n }\n applyCategoryAndPagination(data, options, {\n windowStart,\n reachedBound: source.reachedBound,\n ...(cursored ? { cursorHasMore: cursored.hasMore } : {}),\n })\n return data\n }\n\n const collectionDoc = await findContentCollection(hostId, collectionSlug)\n if (!collectionDoc) return data\n data.collection = mapCollectionDoc(collectionDoc, collectionSlug)\n\n const entryQuery = await collectionDoc.ref\n .collection('entries')\n .where('slug', '==', entrySlug)\n .limit(5)\n .get()\n // Same two-step as `listLiveEntries`: only pay for the org read when\n // something is due, and record the refusal on its own pass because a\n // refused entry never becomes the `entryDoc` below (AGL-471).\n //\n // A PREVIEW RENDER SKIPS BOTH WRITES (AGL-3205). `flipDueEntry` is the\n // entire publishing mechanism for a content entry, and it is a mechanism\n // that belongs to the PUBLIC render: a preview is one person looking at\n // one post through a link, and it must not be able to publish it, nor to\n // burn its schedule with the terminal refusal marker. Skipping the writes\n // costs nothing — the next public render asks the same questions and\n // writes the same answers — and it is what keeps \"nothing but a render\n // publishes a content entry\" true of renders anybody can reach.\n const dueHere = entryQuery.docs.filter((docSnapshot) =>\n isDueScheduled(docSnapshot.data()),\n )\n const permission: SchedulePermission = dueHere.length\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n if (permission !== 'allowed' && !preview) {\n for (const docSnapshot of dueHere) {\n flipDueEntry(docSnapshot.ref, docSnapshot.data(), permission)\n }\n }\n /**\n * What a grant reveals: an entry this collection HOLDS and the site does\n * not serve.\n *\n * Deliberately not routed through `isLive`, which is the public answer and\n * has one other reader (`entry-link-routes`) whose whole job is to agree\n * with it. A second, wider answer inside it would make a LINK to a\n * previewed post resolve on the public site.\n *\n * Every status but `published` qualifies, which is `draft` as well as\n * `scheduled`: the ask is \"let me see it before it goes out\", and a post\n * is most worth looking at before its schedule is set. The blast radius is\n * the same either way — one entry, named in the signature, on one host.\n */\n const entryDoc =\n entryQuery.docs.find((docSnapshot) =>\n isLive(docSnapshot.data(), permission),\n ) ?? (preview ? entryQuery.docs[0] : undefined)\n if (entryDoc) {\n const value = entryDoc.data()\n if (!preview) flipDueEntry(entryDoc.ref, value, permission)\n if (preview && !isLive(value, permission)) {\n // The facts the preview chrome states back to the reader, so the page\n // cannot be mistaken for the published post. Read from the stored\n // document rather than inferred from the render.\n data.entryPreview = {\n status: String(value['status'] ?? 'draft'),\n publishAtSeconds:\n typeof value['publishAt']?.seconds === 'number'\n ? value['publishAt'].seconds\n : null,\n }\n }\n data.entry = {\n $id: entryDoc.id,\n title: value['title'] ?? entrySlug,\n slug: entrySlug,\n body: value['body'] ?? '',\n ...mapEntryFields(value),\n publishedAt: (value['publishedAt'] ?? value['publishAt'])\n ? {\n seconds: (value['publishedAt'] ?? value['publishAt']).seconds,\n }\n : null,\n }\n await attachEntryAuthors(hostId, [data.entry])\n }\n } catch (error) {\n console.error(error)\n data.error = error\n }\n return data\n}\n\nexport default getCollectionContent\n"],"names":["AUTHORS_MAX_PER_HOST","collectionCategorySlug","checkEntitlement","COLLECTION_SOURCE_MAX","collectionTotalPages","entryMatchesCategoryRoute","hostCollectionKind","normalizeContentAuthor","normalizeContentSchemaType","resolveCollectionCategoryBySlug","resolveEntryAuthor","firebaseAdmin","getOrgForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","COLLECTION_SOURCE_TTL_SECONDS","findContentCollection","hostId","collectionSlug","matches","app","firestore","collection","doc","where","limit","get","docs","find","docSnapshot","data","mapEntryFields","value","excerpt","authorName","authorId","coverImage","coverImageAlt","coverVideo","seoTitle","seoDescription","categoryId","category","tags","Array","isArray","filter","tag","updatedAt","seconds","mapCollectionCategories","item","id","trim","name","map","description","mapCollectionDoc","collectionDoc","$id","displayName","slug","templateScreenId","undefined","listScreenId","entryScreenId","schemaType","categories","attachEntryAuthors","entries","ids","Set","entry","Boolean","slice","authors","length","authorsRef","snapshots","getAll","snapshot","exists","author","error","console","ENTRY_SCHEDULE_SKIPPED","scheduleAlreadyRefused","isDueScheduled","Number","POSITIVE_INFINITY","Date","now","isPendingScheduled","scheduledPublishingPermission","org","isLive","permission","flipDueEntry","docRef","update","scheduleStatus","catch","status","publishedAt","LIVE_ENTRY_FIELDS","SCHEDULED_SOURCE_MAX","liveEntriesBase","entriesRef","select","readLiveEntryDocs","dated","orderBy","scheduled","reachedBound","undated","entryDoc","seen","has","add","push","unordered","toLiveEntries","ref","title","sort","a","b","listLiveEntries","due","pendingSchedule","some","readCollectionListingPage","options","cursorId","before","after","cursorDoc","backwards","startAfter","hasMore","pageDocs","reverse","resolveCollectionPageCursor","skip","page","perPage","isFinite","offset","getPublishedCollectionSource","key","revalidate","read","readPublishedCollectionSource","store","getPublishedCollectionEntries","applyCategoryAndPagination","listing","routedCategory","categorySlug","match","known","windowStart","windowed","pageEnd","last","first","cursorHasMore","countable","totalEntries","pagination","nextCursor","prevCursor","totalPages","getCollectionContent","entrySlug","preview","previewUnpublishedEntry","timeZone","entryQuery","source","entriesReachedBound","cursor","cursored","dueHere","entryPreview","String","publishAtSeconds","body"],"mappings":"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"}