@aglyn/tenant-runtime 1.0.0-beta.146 → 1.0.0-beta.149

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.146",
3
+ "version": "1.0.0-beta.149",
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.146",
29
- "@aglyn/tenant-data-admin": "1.0.0-beta.146",
28
+ "@aglyn/aglyn": "1.0.0-beta.149",
29
+ "@aglyn/tenant-data-admin": "1.0.0-beta.149",
30
30
  "@swc/helpers": "0.5.23"
31
31
  },
32
32
  "peerDependencies": {
@@ -61,7 +61,12 @@ hostId?: string): NodesMap;
61
61
  export interface FallbackListPagination {
62
62
  page: number;
63
63
  perPage: number;
64
- totalPages: number;
64
+ /** @deprecated AGL-3219 — absent past one read's bound. */
65
+ totalPages?: number;
66
+ /** The older page's cursor, or `''`/absent when there is none (AGL-3219). */
67
+ nextCursor?: string;
68
+ /** The newer page's cursor. */
69
+ prevCursor?: string;
65
70
  }
66
71
  /** The routed category for a filtered listing (AGL-1321). */
67
72
  export interface FallbackListCategory {
@@ -377,7 +377,16 @@ const typography = (suffix, props)=>[
377
377
  ]);
378
378
  return _extends({}, shell(entries.map(([entryId])=>entryId), entryShell()), Object.fromEntries(entries));
379
379
  }
380
- /** Prev/next + "Page X of Y" nav linking to /{slug}/page/{n} (AGL-620). */ function paginationNodes(collection, pagination, category) {
380
+ /**
381
+ * Prev/next nav over the cursor addresses (AGL-620, AGL-3219).
382
+ *
383
+ * The label says "Page 4 of 9" only where nine is a number that can be known
384
+ * — a collection small enough to be read in one go. Past that it says "Page
385
+ * 4" and stops, because the alternative is a total derived from a count the
386
+ * listing's own reads could disagree with, which is how page 10 of a
387
+ * seventeen-page changelog came to call itself the last one.
388
+ */ function paginationNodes(collection, pagination, category) {
389
+ var _pagination_nextCursor, _pagination_prevCursor;
381
390
  // Page 1 lives at the bare listing; deeper pages at .../page/{n}. Built
382
391
  // through the shared pager computation so this fallback and the
383
392
  // `{{pagination.*}}` tokens an authored template binds (AGL-1386) can never
@@ -390,8 +399,12 @@ const typography = (suffix, props)=>[
390
399
  }, category ? {
391
400
  categorySlug: category.slug
392
401
  } : {}, {
393
- page: pagination.page,
402
+ page: pagination.page
403
+ }, pagination.totalPages === undefined ? {} : {
394
404
  totalPages: pagination.totalPages
405
+ }, {
406
+ nextCursor: (_pagination_nextCursor = pagination.nextCursor) != null ? _pagination_nextCursor : '',
407
+ prevCursor: (_pagination_prevCursor = pagination.prevCursor) != null ? _pagination_prevCursor : ''
395
408
  }));
396
409
  const children = [];
397
410
  const nodes = {};
@@ -415,7 +428,7 @@ const typography = (suffix, props)=>[
415
428
  parentId: id('pager'),
416
429
  props: {
417
430
  variant: 'body2',
418
- children: `Page ${page} of ${totalPages}`,
431
+ children: totalPages > 1 ? `Page ${page} of ${totalPages}` : `Page ${page}`,
419
432
  sx: {
420
433
  color: 'text.secondary'
421
434
  }
@@ -463,6 +476,7 @@ const typography = (suffix, props)=>[
463
476
  * `pagination` (AGL-620) the block renders one page and prev/next nav links
464
477
  * to `/{slug}/page/{n}`.
465
478
  */ export function buildCollectionListFallbackNodes(collection, hasEntries, pagination, category) {
479
+ var _pagination_totalPages;
466
480
  var _collection_categories;
467
481
  const [titleId, title] = typography('title', {
468
482
  variant: 'h3',
@@ -517,7 +531,12 @@ const typography = (suffix, props)=>[
517
531
  parentId: id('item'),
518
532
  props
519
533
  });
520
- const pager = pagination && pagination.totalPages > 1 ? paginationNodes(collection, pagination, category) : null;
534
+ /*
535
+ * A pager exists when there is somewhere to go (AGL-3219): an older page,
536
+ * proved by the cursor probe, or a newer one, proved by being past the
537
+ * first. `totalPages > 1` used to stand in for that and could not, once the
538
+ * total stopped being knowable.
539
+ */ const pager = pagination && (Boolean(pagination.nextCursor) || pagination.page > 1 || ((_pagination_totalPages = pagination.totalPages) != null ? _pagination_totalPages : 0) > 1) ? paginationNodes(collection, pagination, category) : null;
521
540
  return _extends({}, shell(pager ? [
522
541
  ...lead,
523
542
  id('entries'),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/collection-fallback-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { wistiaMediaId } from '@aglyn/aglyn/app-utils/wistia-embed'\nimport * as Aglyn from '@aglyn/aglyn/server'\n\n/** Namespaces the synthetic fallback node ids (never persisted). */\nconst FALLBACK_ID_PREFIX = 'cfb__'\n\nconst id = (suffix: string) => `${FALLBACK_ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\ninterface FallbackCollection {\n slug: string\n displayName: string\n /** Category taxonomy (AGL-582) for `categoryId` → name resolution. */\n categories?: Aglyn.CollectionCategory[]\n}\n\ninterface FallbackEntry {\n title?: string\n slug?: string\n excerpt?: string\n /** Per-entry byline (AGL-686); falls back to the site as author. */\n authorName?: string\n body?: string\n coverImage?: string\n /** The featured video (AGL-2956); plays in the cover's place. */\n coverVideo?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /** Stable taxonomy reference (AGL-582); wins over `category`. */\n categoryId?: string\n /** Legacy free-typed category (AGL-582); read-only fallback. */\n category?: string\n tags?: string[]\n publishedAt?: { seconds: number } | null\n}\n\n// The one shared formatter (AGL-1926). This composes server-side, so the\n// inline `toLocaleDateString()` it replaces was not itself a hydration\n// mismatch — but it was a THIRD spelling of \"the entry's published date\",\n// free to drift from the byline and the related-post card that quote the\n// same instant. Pinned locale and zone, so all three agree by construction.\nconst formatDate = (value?: { seconds: number } | null) =>\n Aglyn.formatCollectionEntryDate(value)\n\n/**\n * What may be interpolated into the cover block's CSS `url(\"…\")` (AGL-1407).\n *\n * The gate this replaces was `/^https?:\\/\\//i`, which answered the safety\n * question correctly and the coverage question wrongly: it admitted only the\n * OLDEST of the stored generations. A `media:` reference failed it and the\n * cover block was never emitted — a SILENT drop, no error, no placeholder,\n * just a page missing its picture — and so did the AGL-175 relative CDN path\n * that the first pass at media references wrote. Resolving before testing is\n * what closes that, and the test then has to accept a site-relative path,\n * which is what {@link Aglyn.resolveMediaSrc} returns for a reference.\n *\n * Still a scheme allowlist rather than \"anything non-empty\": this value lands\n * inside a stylesheet rather than in an `<img src>`, so `data:` and friends\n * have no business here even though {@link cssUrlValue} already makes\n * quote-breaking impossible. A bare relative `cover.png` stays rejected\n * exactly as before — there is no page for it to resolve against at compose\n * time.\n *\n * `http:` was dropped for AGL-1725. AGL-1701 found two paths accepting it;\n * markdown-lite is AGL-1713 and this was the second, separate one. It is the\n * only scheme change here, and it is not a host restriction: an author\n * hotlinking their own https cover keeps working, because on a published\n * site the author IS the site owner and the audience is their own visitors.\n * `http:` alone has no defensible use — the request is mixed passive content\n * that current browsers already block or auto-upgrade, so the author gains\n * nothing from it, while the plaintext fetch discloses which article a\n * visitor is reading to every observer on the network path.\n *\n * The consequence of the drop is the SILENT DROP this whole comment is about,\n * for `http:` covers only: no cover node is emitted. That is the correct\n * trade here and the browser was very likely refusing the image anyway, but\n * it is the reason the change is one scheme and not a host list.\n */\nconst RENDERABLE_COVER_URL = /^(?:https:\\/\\/|\\/)/i\n\n/**\n * A URL made safe to sit inside `url(\"…\")`, WITHOUT re-encoding it.\n *\n * This replaces `encodeURI`, which was the wrong tool and quietly broke the\n * commonest stored form: `encodeURI` escapes `%`, so every raw firebasestorage\n * download URL — whose object path is `orgs%2F…%2Fmedia%2F…` — came out\n * double-encoded as `%252F` and 404'd. The block rendered, the image did not,\n * which is why it survived: the page looks composed and the picture is just\n * missing.\n *\n * Only the characters that could END the CSS string are escaped, and they are\n * escaped percent-wise rather than with a backslash so the result is still a\n * valid URL if anything downstream reads it as one. `encodeURI` was never\n * needed for anything else here — a stored URL is already URL-encoded.\n */\nconst cssUrlValue = (url: string) =>\n url.replace(/[\"\\\\\\n\\r]/g, (char) => encodeURIComponent(char))\n\n/**\n * Container width for the built-in entry article: the PROSE case of the\n * Container standard (AGL-1298), not an arbitrary narrow default. A\n * collection entry is an article body, and `xl` — the section default — runs\n * a paragraph past 180 characters a line. Stock breakpoint, no bespoke\n * number.\n */\nconst ENTRY_MAX_WIDTH = 'md'\n\n/**\n * Container width for the built-in listing: the section default. A listing is\n * a card grid rather than a reading column, so the entry's prose width is the\n * wrong measure for it, and the authored list screens render at this same\n * `xl` (AGL-2567). Still a stock breakpoint, so the AGL-1298 rule holds.\n */\nconst LIST_MAX_WIDTH = 'xl'\n\n/**\n * Vertical rhythm between the built-in listing's sections, in MUI theme\n * spacing units: `theme.spacing(6)` resolves to 48px, the same rhythm the\n * authored list screens carry, so a generated listing and an authored one\n * read as one product (AGL-2567).\n *\n * A theme token rather than a pixel string: a hand-written length here\n * answers to no theme, and a length missing its unit is a valid-looking\n * value the browser drops.\n *\n * @see https://mui.com/material-ui/customization/spacing/\n */\nconst LIST_SECTION_GAP = 6\n\n/** The page shell's width and the rhythm of the stack that holds sections. */\ninterface ShellLayout {\n /** Stock Container breakpoint (AGL-1298); never a bespoke pixel cap. */\n maxWidth: typeof ENTRY_MAX_WIDTH | typeof LIST_MAX_WIDTH\n /** Props for the content stack the page's sections slot into. */\n stackProps: Record<string, unknown>\n}\n\n/** The entry article's shell: reading column, sections close together. */\nconst entryShell = (): ShellLayout => ({\n maxWidth: ENTRY_MAX_WIDTH,\n stackProps: { spacing: 2 },\n})\n\n/**\n * The listing's shell: full section width, sections on the 48px rhythm.\n *\n * The gap is declared on the stack's `sx` rather than left to the Stack's own\n * `spacing`, which is the shape the authored list screens carry: one\n * `row-gap` on the flex container, so every section is spaced by the same\n * declaration instead of by margins that a section's own styling can collapse\n * or override.\n */\nconst listShell = (): ShellLayout => ({\n maxWidth: LIST_MAX_WIDTH,\n stackProps: {\n sx: {\n display: 'flex',\n flexDirection: 'column',\n gap: LIST_SECTION_GAP,\n },\n },\n})\n\n/** Root → centered container → content stack; children slot underneath. */\nfunction shell(childIds: string[], layout: ShellLayout): NodesMap {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n },\n [id('container')]: {\n $id: id('container'),\n componentId: 'muiContainer',\n pluginId: 'mui',\n parentId: Aglyn.NODE_ROOT_ID,\n props: {\n maxWidth: layout.maxWidth,\n sx: { paddingTop: 6, paddingBottom: 6 },\n },\n nodes: [id('stack')],\n },\n [id('stack')]: {\n $id: id('stack'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('container'),\n props: layout.stackProps,\n nodes: childIds,\n },\n }\n}\n\nconst typography = (\n suffix: string,\n props: Record<string, unknown>,\n): AglynNodeEntry => [\n id(suffix),\n {\n $id: id(suffix),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('stack'),\n props,\n },\n]\n\ntype AglynNodeEntry = [string, Aglyn.AglynNodeSchema]\n\n/**\n * The Video node's props for an entry's featured video, or `undefined` when\n * the entry has none this page can play (AGL-2956).\n *\n * An entry of a video collection is an article whose film sits where a post's\n * cover would, and on the built-in page nobody is there to bind one. So the\n * props carry what a designer would bind by hand: the source, the cover as\n * the poster, and the three facts the page's `VideoObject` is built from —\n * the title, the SEO description falling back to the excerpt as\n * `{{entry.seoDescription}}` does, and the ISO publish instant\n * `{{entry.publishedAt}}` resolves to. An instant rather than a calendar day:\n * the builder publishes `uploadDate` as a date-time with a zone, and it keeps\n * an instant as given where it would publish a bare day at noon UTC.\n *\n * `undefined` rather than a Video node in two cases, because the element's\n * answer to each is a labeled placeholder, which a published article must\n * never show:\n *\n * - a source nothing resolves, such as an empty field or a malformed `media:`\n * reference;\n * - a Wistia link with no poster. Its player loads only when a visitor\n * presses the poster, so without one there is nothing to press.\n *\n * The page then renders as a cover article, exactly as it does for an entry\n * with no video.\n *\n * The poster is the stored reference, not a resolved URL, so the element and\n * the structured data each resolve it against the site rendering the page. It\n * is passed only when the cover's own scheme rule would render it, so this\n * page never shows a still as a poster that it refuses as a cover.\n *\n * The film takes the cover's whole slot in the reading column, whose stock\n * `md` container bounds it; the column is the width limit rather than a pixel\n * cap (AGL-1298).\n */\nfunction featuredVideoProps(\n entry: FallbackEntry,\n options: { hostId?: string; posterRenders: boolean },\n): Record<string, unknown> | undefined {\n const src = (entry.coverVideo ?? '').trim()\n if (!src || !Aglyn.resolveMediaSrc(src, { hostId: options.hostId })) {\n return undefined\n }\n const poster = options.posterRenders ? entry.coverImage : undefined\n if (wistiaMediaId(src) && !poster) return undefined\n const description = entry.seoDescription || entry.excerpt || ''\n const uploadDate = Aglyn.collectionEntryPublishedAtIso(entry.publishedAt)\n return {\n src,\n ...(poster ? { poster } : {}),\n title: entry.title ?? '',\n ...(description ? { description } : {}),\n ...(uploadDate ? { uploadDate } : {}),\n width: '100%',\n }\n}\n\n/**\n * Built-in entry article as canvas nodes (AGL-551): when a collection has\n * no entry-template screen, `/{collection}/{entry}` renders these through\n * the normal compose pipeline — site theme, shared layout chrome, and the\n * markdown-rendering Entry body block — instead of the old unthemed HTML.\n */\nexport function buildCollectionEntryFallbackNodes(\n collection: FallbackCollection,\n entry: FallbackEntry,\n /**\n * The site being composed, so an org-scoped reference is host-qualified the\n * way every other render surface qualifies one (AGL-1043/AGL-1407). Optional\n * because a bare org reference still resolves without it; passing it is what\n * lets an asset restricted to this site resolve at all.\n */\n hostId?: string,\n): NodesMap {\n const entries: AglynNodeEntry[] = [\n typography('title', {\n variant: 'h3',\n component: 'h1',\n children: entry.title ?? '',\n }),\n ]\n const date = formatDate(entry.publishedAt)\n const tags = (entry.tags ?? []).filter(Boolean)\n // Category name resolves against the collection's taxonomy (AGL-582):\n // `categoryId` lookup first, legacy free-typed string fallback.\n const categoryName =\n Aglyn.resolveEntryCategoryName(entry, collection.categories) ?? ''\n if (date || categoryName || tags.length) {\n // Entry meta block (AGL-582): \"date · category\" line + tag chips.\n entries.push([\n id('meta'),\n {\n $id: id('meta'),\n componentId: Aglyn.COLLECTION_ENTRY_META_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n date,\n category: categoryName,\n tags: tags.join(', '),\n },\n },\n ])\n }\n // The cover, through the ONE shared resolver (AGL-1407). A `media:`\n // reference becomes the site-relative CDN path; a raw storage URL, an\n // AGL-175 CDN path and an author's own hotlinked URL all pass through\n // untouched, which is the documented precedence in `media-ref.ts`.\n const coverImage = Aglyn.resolveMediaSrc(entry.coverImage, { hostId })\n const coverRenders = Boolean(\n coverImage && RENDERABLE_COVER_URL.test(coverImage),\n )\n // A featured video takes the cover's slot, with the cover as its poster\n // (AGL-2956); see `featuredVideoProps` for when an entry has one to play.\n const video = featuredVideoProps(entry, {\n hostId,\n posterRenders: coverRenders,\n })\n if (video) {\n entries.push([\n id('video'),\n {\n $id: id('video'),\n componentId: Aglyn.VIDEO_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: video,\n },\n ])\n } else if (coverRenders) {\n // Rendered as a background image on a plain stack, NOT through the\n // first-party `image` component — that component crashes tenant SSR\n // (AGL-579); the background-image approach is proven safe.\n entries.push([\n id('cover'),\n {\n $id: id('cover'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n role: 'img',\n 'aria-label': entry.title ?? '',\n sx: {\n backgroundImage: `url(\"${cssUrlValue(coverImage)}\")`,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n borderRadius: 1,\n minHeight: 320,\n },\n },\n },\n ])\n }\n entries.push([\n id('body'),\n {\n $id: id('body'),\n componentId: Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { markdown: entry.body ?? '' },\n },\n ])\n // Related posts after the body, share bar at the end (AGL-582); the\n // compose pipeline stamps the related entries server-side.\n entries.push([\n id('related'),\n {\n $id: id('related'),\n componentId: Aglyn.COLLECTION_RELATED_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { heading: 'Related articles', limit: 3, sx: { paddingTop: 2 } },\n },\n ])\n entries.push([\n id('share'),\n {\n $id: id('share'),\n componentId: Aglyn.COLLECTION_SHARE_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {},\n },\n ])\n entries.push([\n id('back'),\n {\n $id: id('back'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n href: `/${collection.slug}`,\n children: `← ${collection.displayName}`,\n sx: { alignSelf: 'flex-start' },\n },\n },\n ])\n return {\n ...shell(\n entries.map(([entryId]) => entryId),\n entryShell(),\n ),\n ...Object.fromEntries(entries),\n }\n}\n\n/** Pagination state for the built-in list (AGL-620). */\nexport interface FallbackListPagination {\n page: number\n perPage: number\n totalPages: number\n}\n\n/** The routed category for a filtered listing (AGL-1321). */\nexport interface FallbackListCategory {\n slug: string\n name: string\n}\n\n/** Prev/next + \"Page X of Y\" nav linking to /{slug}/page/{n} (AGL-620). */\nfunction paginationNodes(\n collection: FallbackCollection,\n pagination: FallbackListPagination,\n category?: FallbackListCategory,\n): { childId: string; nodes: NodesMap } {\n // Page 1 lives at the bare listing; deeper pages at .../page/{n}. Built\n // through the shared pager computation so this fallback and the\n // `{{pagination.*}}` tokens an authored template binds (AGL-1386) can never\n // disagree — and so the pager stays inside the category it is paging\n // (AGL-1321): a \"next\" that dropped the filter would silently return the\n // reader to the unfiltered list. An edge is the empty string, which is\n // exactly the \"no link here\" this fallback already meant.\n const { page, totalPages, prevUrl, nextUrl } = Aglyn.collectionPaginationLinks(\n {\n collectionSlug: collection.slug,\n ...(category ? { categorySlug: category.slug } : {}),\n page: pagination.page,\n totalPages: pagination.totalPages,\n },\n )\n const children: string[] = []\n const nodes: NodesMap = {}\n if (prevUrl) {\n nodes[id('prev')] = {\n $id: id('prev'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: prevUrl, children: '← Newer' },\n }\n children.push(id('prev'))\n }\n nodes[id('pageinfo')] = {\n $id: id('pageinfo'),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('pager'),\n props: {\n variant: 'body2',\n children: `Page ${page} of ${totalPages}`,\n sx: { color: 'text.secondary' },\n },\n }\n children.push(id('pageinfo'))\n if (nextUrl) {\n nodes[id('next')] = {\n $id: id('next'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: nextUrl, children: 'Older →' },\n }\n children.push(id('next'))\n }\n nodes[id('pager')] = {\n $id: id('pager'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n direction: 'row',\n spacing: 3,\n sx: { alignItems: 'center', justifyContent: 'center' },\n },\n nodes: children,\n }\n return { childId: id('pager'), nodes }\n}\n\n/**\n * Built-in entry list as canvas nodes (AGL-551): a heading plus a\n * Collection entries block whose template (title, date, excerpt, Read\n * more) the compose pipeline expands over the published entries — the same\n * block designers drop onto their own list-template screens. With\n * `pagination` (AGL-620) the block renders one page and prev/next nav links\n * to `/{slug}/page/{n}`.\n */\nexport function buildCollectionListFallbackNodes(\n collection: FallbackCollection,\n hasEntries: boolean,\n pagination?: FallbackListPagination,\n category?: FallbackListCategory,\n): NodesMap {\n const [titleId, title] = typography('title', {\n variant: 'h3',\n component: 'h1',\n // A filtered listing says which slice it is (AGL-1321) — a page of\n // Guides whose only heading reads \"Blog\" tells the reader nothing about\n // why the other posts vanished.\n children: category\n ? `${collection.displayName} · ${category.name}`\n : collection.displayName,\n })\n // Category pills (AGL-1321): stamped by `expandCollectionCategories` during\n // compose, from the same taxonomy this node's collection carries. Present\n // in the EMPTY case too — a filtered listing with no entries whose pills\n // vanished would be a dead end with no way back to \"All\".\n const pills = collection.categories?.length\n ? {\n $id: id('pills'),\n componentId: Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { allLabel: 'All' },\n }\n : null\n const lead = pills ? [titleId, id('pills')] : [titleId]\n if (!hasEntries) {\n const [emptyId, empty] = typography('empty', {\n variant: 'body1',\n children: category\n ? `Nothing published in ${category.name} yet.`\n : 'Nothing published yet.',\n sx: { color: 'text.secondary' },\n })\n return {\n ...shell([...lead, emptyId], listShell()),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [emptyId]: empty,\n }\n }\n const item = (suffix: string, props: Record<string, unknown>) => ({\n $id: id(suffix),\n componentId: 'muiTypography' as const,\n pluginId: 'mui',\n parentId: id('item'),\n props,\n })\n const pager =\n pagination && pagination.totalPages > 1\n ? paginationNodes(collection, pagination, category)\n : null\n return {\n ...shell(\n pager\n ? [...lead, id('entries'), pager.childId]\n : [...lead, id('entries')],\n listShell(),\n ),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [id('entries')]: {\n $id: id('entries'),\n componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n spacing: 4,\n ...(pagination\n ? { perPage: pagination.perPage, page: pagination.page }\n : {}),\n },\n nodes: [id('item')],\n },\n ...(pager ? pager.nodes : {}),\n [id('item')]: {\n $id: id('item'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('entries'),\n props: { spacing: 0.5 },\n nodes: [\n id('item-title'),\n id('item-date'),\n id('item-excerpt'),\n id('item-link'),\n ],\n },\n [id('item-title')]: item('item-title', {\n variant: 'h5',\n component: 'h2',\n children: '{{entry.title}}',\n }),\n [id('item-date')]: item('item-date', {\n variant: 'caption',\n children: '{{entry.date}}',\n sx: { color: 'text.secondary' },\n }),\n [id('item-excerpt')]: item('item-excerpt', {\n variant: 'body1',\n children: '{{entry.excerpt}}',\n }),\n [id('item-link')]: {\n $id: id('item-link'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('item'),\n props: {\n href: '{{entry.url}}',\n children: 'Read more',\n size: 'small',\n sx: { alignSelf: 'flex-start' },\n },\n },\n }\n}\n\n/** Entry vs list fallback selection for the routed content (AGL-551). */\nexport function buildCollectionFallbackNodes(content: {\n collection: FallbackCollection\n entries: FallbackEntry[]\n entry: FallbackEntry | null\n pagination?: FallbackListPagination | null\n category?: FallbackListCategory | null\n /** The composing site, for host-qualifying a media reference (AGL-1407). */\n hostId?: string\n}): NodesMap {\n return content.entry\n ? buildCollectionEntryFallbackNodes(\n content.collection,\n content.entry,\n content.hostId,\n )\n : buildCollectionListFallbackNodes(\n content.collection,\n content.entries.length > 0,\n content.pagination ?? undefined,\n content.category ?? undefined,\n )\n}\n\nexport default buildCollectionFallbackNodes\n"],"names":["wistiaMediaId","Aglyn","FALLBACK_ID_PREFIX","id","suffix","formatDate","value","formatCollectionEntryDate","RENDERABLE_COVER_URL","cssUrlValue","url","replace","char","encodeURIComponent","ENTRY_MAX_WIDTH","LIST_MAX_WIDTH","LIST_SECTION_GAP","entryShell","maxWidth","stackProps","spacing","listShell","sx","display","flexDirection","gap","shell","childIds","layout","NODE_ROOT_ID","$id","componentId","nodes","pluginId","parentId","props","paddingTop","paddingBottom","typography","featuredVideoProps","entry","options","src","coverVideo","trim","resolveMediaSrc","hostId","undefined","poster","posterRenders","coverImage","description","seoDescription","excerpt","uploadDate","collectionEntryPublishedAtIso","publishedAt","title","width","buildCollectionEntryFallbackNodes","collection","entries","variant","component","children","date","tags","filter","Boolean","categoryName","resolveEntryCategoryName","categories","length","push","COLLECTION_ENTRY_META_COMPONENT_ID","category","join","coverRenders","test","video","VIDEO_COMPONENT_ID","role","backgroundImage","backgroundSize","backgroundPosition","borderRadius","minHeight","COLLECTION_ENTRY_BODY_COMPONENT_ID","markdown","body","COLLECTION_RELATED_COMPONENT_ID","heading","limit","COLLECTION_SHARE_COMPONENT_ID","href","slug","displayName","alignSelf","map","entryId","Object","fromEntries","paginationNodes","pagination","page","totalPages","prevUrl","nextUrl","collectionPaginationLinks","collectionSlug","categorySlug","color","direction","alignItems","justifyContent","childId","buildCollectionListFallbackNodes","hasEntries","titleId","name","pills","COLLECTION_CATEGORIES_COMPONENT_ID","allLabel","lead","emptyId","empty","item","pager","COLLECTION_ENTRIES_COMPONENT_ID","perPage","size","buildCollectionFallbackNodes","content"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/collection-fallback-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { wistiaMediaId } from '@aglyn/aglyn/app-utils/wistia-embed'\nimport * as Aglyn from '@aglyn/aglyn/server'\n\n/** Namespaces the synthetic fallback node ids (never persisted). */\nconst FALLBACK_ID_PREFIX = 'cfb__'\n\nconst id = (suffix: string) => `${FALLBACK_ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\ninterface FallbackCollection {\n slug: string\n displayName: string\n /** Category taxonomy (AGL-582) for `categoryId` → name resolution. */\n categories?: Aglyn.CollectionCategory[]\n}\n\ninterface FallbackEntry {\n title?: string\n slug?: string\n excerpt?: string\n /** Per-entry byline (AGL-686); falls back to the site as author. */\n authorName?: string\n body?: string\n coverImage?: string\n /** The featured video (AGL-2956); plays in the cover's place. */\n coverVideo?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /** Stable taxonomy reference (AGL-582); wins over `category`. */\n categoryId?: string\n /** Legacy free-typed category (AGL-582); read-only fallback. */\n category?: string\n tags?: string[]\n publishedAt?: { seconds: number } | null\n}\n\n// The one shared formatter (AGL-1926). This composes server-side, so the\n// inline `toLocaleDateString()` it replaces was not itself a hydration\n// mismatch — but it was a THIRD spelling of \"the entry's published date\",\n// free to drift from the byline and the related-post card that quote the\n// same instant. Pinned locale and zone, so all three agree by construction.\nconst formatDate = (value?: { seconds: number } | null) =>\n Aglyn.formatCollectionEntryDate(value)\n\n/**\n * What may be interpolated into the cover block's CSS `url(\"…\")` (AGL-1407).\n *\n * The gate this replaces was `/^https?:\\/\\//i`, which answered the safety\n * question correctly and the coverage question wrongly: it admitted only the\n * OLDEST of the stored generations. A `media:` reference failed it and the\n * cover block was never emitted — a SILENT drop, no error, no placeholder,\n * just a page missing its picture — and so did the AGL-175 relative CDN path\n * that the first pass at media references wrote. Resolving before testing is\n * what closes that, and the test then has to accept a site-relative path,\n * which is what {@link Aglyn.resolveMediaSrc} returns for a reference.\n *\n * Still a scheme allowlist rather than \"anything non-empty\": this value lands\n * inside a stylesheet rather than in an `<img src>`, so `data:` and friends\n * have no business here even though {@link cssUrlValue} already makes\n * quote-breaking impossible. A bare relative `cover.png` stays rejected\n * exactly as before — there is no page for it to resolve against at compose\n * time.\n *\n * `http:` was dropped for AGL-1725. AGL-1701 found two paths accepting it;\n * markdown-lite is AGL-1713 and this was the second, separate one. It is the\n * only scheme change here, and it is not a host restriction: an author\n * hotlinking their own https cover keeps working, because on a published\n * site the author IS the site owner and the audience is their own visitors.\n * `http:` alone has no defensible use — the request is mixed passive content\n * that current browsers already block or auto-upgrade, so the author gains\n * nothing from it, while the plaintext fetch discloses which article a\n * visitor is reading to every observer on the network path.\n *\n * The consequence of the drop is the SILENT DROP this whole comment is about,\n * for `http:` covers only: no cover node is emitted. That is the correct\n * trade here and the browser was very likely refusing the image anyway, but\n * it is the reason the change is one scheme and not a host list.\n */\nconst RENDERABLE_COVER_URL = /^(?:https:\\/\\/|\\/)/i\n\n/**\n * A URL made safe to sit inside `url(\"…\")`, WITHOUT re-encoding it.\n *\n * This replaces `encodeURI`, which was the wrong tool and quietly broke the\n * commonest stored form: `encodeURI` escapes `%`, so every raw firebasestorage\n * download URL — whose object path is `orgs%2F…%2Fmedia%2F…` — came out\n * double-encoded as `%252F` and 404'd. The block rendered, the image did not,\n * which is why it survived: the page looks composed and the picture is just\n * missing.\n *\n * Only the characters that could END the CSS string are escaped, and they are\n * escaped percent-wise rather than with a backslash so the result is still a\n * valid URL if anything downstream reads it as one. `encodeURI` was never\n * needed for anything else here — a stored URL is already URL-encoded.\n */\nconst cssUrlValue = (url: string) =>\n url.replace(/[\"\\\\\\n\\r]/g, (char) => encodeURIComponent(char))\n\n/**\n * Container width for the built-in entry article: the PROSE case of the\n * Container standard (AGL-1298), not an arbitrary narrow default. A\n * collection entry is an article body, and `xl` — the section default — runs\n * a paragraph past 180 characters a line. Stock breakpoint, no bespoke\n * number.\n */\nconst ENTRY_MAX_WIDTH = 'md'\n\n/**\n * Container width for the built-in listing: the section default. A listing is\n * a card grid rather than a reading column, so the entry's prose width is the\n * wrong measure for it, and the authored list screens render at this same\n * `xl` (AGL-2567). Still a stock breakpoint, so the AGL-1298 rule holds.\n */\nconst LIST_MAX_WIDTH = 'xl'\n\n/**\n * Vertical rhythm between the built-in listing's sections, in MUI theme\n * spacing units: `theme.spacing(6)` resolves to 48px, the same rhythm the\n * authored list screens carry, so a generated listing and an authored one\n * read as one product (AGL-2567).\n *\n * A theme token rather than a pixel string: a hand-written length here\n * answers to no theme, and a length missing its unit is a valid-looking\n * value the browser drops.\n *\n * @see https://mui.com/material-ui/customization/spacing/\n */\nconst LIST_SECTION_GAP = 6\n\n/** The page shell's width and the rhythm of the stack that holds sections. */\ninterface ShellLayout {\n /** Stock Container breakpoint (AGL-1298); never a bespoke pixel cap. */\n maxWidth: typeof ENTRY_MAX_WIDTH | typeof LIST_MAX_WIDTH\n /** Props for the content stack the page's sections slot into. */\n stackProps: Record<string, unknown>\n}\n\n/** The entry article's shell: reading column, sections close together. */\nconst entryShell = (): ShellLayout => ({\n maxWidth: ENTRY_MAX_WIDTH,\n stackProps: { spacing: 2 },\n})\n\n/**\n * The listing's shell: full section width, sections on the 48px rhythm.\n *\n * The gap is declared on the stack's `sx` rather than left to the Stack's own\n * `spacing`, which is the shape the authored list screens carry: one\n * `row-gap` on the flex container, so every section is spaced by the same\n * declaration instead of by margins that a section's own styling can collapse\n * or override.\n */\nconst listShell = (): ShellLayout => ({\n maxWidth: LIST_MAX_WIDTH,\n stackProps: {\n sx: {\n display: 'flex',\n flexDirection: 'column',\n gap: LIST_SECTION_GAP,\n },\n },\n})\n\n/** Root → centered container → content stack; children slot underneath. */\nfunction shell(childIds: string[], layout: ShellLayout): NodesMap {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n },\n [id('container')]: {\n $id: id('container'),\n componentId: 'muiContainer',\n pluginId: 'mui',\n parentId: Aglyn.NODE_ROOT_ID,\n props: {\n maxWidth: layout.maxWidth,\n sx: { paddingTop: 6, paddingBottom: 6 },\n },\n nodes: [id('stack')],\n },\n [id('stack')]: {\n $id: id('stack'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('container'),\n props: layout.stackProps,\n nodes: childIds,\n },\n }\n}\n\nconst typography = (\n suffix: string,\n props: Record<string, unknown>,\n): AglynNodeEntry => [\n id(suffix),\n {\n $id: id(suffix),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('stack'),\n props,\n },\n]\n\ntype AglynNodeEntry = [string, Aglyn.AglynNodeSchema]\n\n/**\n * The Video node's props for an entry's featured video, or `undefined` when\n * the entry has none this page can play (AGL-2956).\n *\n * An entry of a video collection is an article whose film sits where a post's\n * cover would, and on the built-in page nobody is there to bind one. So the\n * props carry what a designer would bind by hand: the source, the cover as\n * the poster, and the three facts the page's `VideoObject` is built from —\n * the title, the SEO description falling back to the excerpt as\n * `{{entry.seoDescription}}` does, and the ISO publish instant\n * `{{entry.publishedAt}}` resolves to. An instant rather than a calendar day:\n * the builder publishes `uploadDate` as a date-time with a zone, and it keeps\n * an instant as given where it would publish a bare day at noon UTC.\n *\n * `undefined` rather than a Video node in two cases, because the element's\n * answer to each is a labeled placeholder, which a published article must\n * never show:\n *\n * - a source nothing resolves, such as an empty field or a malformed `media:`\n * reference;\n * - a Wistia link with no poster. Its player loads only when a visitor\n * presses the poster, so without one there is nothing to press.\n *\n * The page then renders as a cover article, exactly as it does for an entry\n * with no video.\n *\n * The poster is the stored reference, not a resolved URL, so the element and\n * the structured data each resolve it against the site rendering the page. It\n * is passed only when the cover's own scheme rule would render it, so this\n * page never shows a still as a poster that it refuses as a cover.\n *\n * The film takes the cover's whole slot in the reading column, whose stock\n * `md` container bounds it; the column is the width limit rather than a pixel\n * cap (AGL-1298).\n */\nfunction featuredVideoProps(\n entry: FallbackEntry,\n options: { hostId?: string; posterRenders: boolean },\n): Record<string, unknown> | undefined {\n const src = (entry.coverVideo ?? '').trim()\n if (!src || !Aglyn.resolveMediaSrc(src, { hostId: options.hostId })) {\n return undefined\n }\n const poster = options.posterRenders ? entry.coverImage : undefined\n if (wistiaMediaId(src) && !poster) return undefined\n const description = entry.seoDescription || entry.excerpt || ''\n const uploadDate = Aglyn.collectionEntryPublishedAtIso(entry.publishedAt)\n return {\n src,\n ...(poster ? { poster } : {}),\n title: entry.title ?? '',\n ...(description ? { description } : {}),\n ...(uploadDate ? { uploadDate } : {}),\n width: '100%',\n }\n}\n\n/**\n * Built-in entry article as canvas nodes (AGL-551): when a collection has\n * no entry-template screen, `/{collection}/{entry}` renders these through\n * the normal compose pipeline — site theme, shared layout chrome, and the\n * markdown-rendering Entry body block — instead of the old unthemed HTML.\n */\nexport function buildCollectionEntryFallbackNodes(\n collection: FallbackCollection,\n entry: FallbackEntry,\n /**\n * The site being composed, so an org-scoped reference is host-qualified the\n * way every other render surface qualifies one (AGL-1043/AGL-1407). Optional\n * because a bare org reference still resolves without it; passing it is what\n * lets an asset restricted to this site resolve at all.\n */\n hostId?: string,\n): NodesMap {\n const entries: AglynNodeEntry[] = [\n typography('title', {\n variant: 'h3',\n component: 'h1',\n children: entry.title ?? '',\n }),\n ]\n const date = formatDate(entry.publishedAt)\n const tags = (entry.tags ?? []).filter(Boolean)\n // Category name resolves against the collection's taxonomy (AGL-582):\n // `categoryId` lookup first, legacy free-typed string fallback.\n const categoryName =\n Aglyn.resolveEntryCategoryName(entry, collection.categories) ?? ''\n if (date || categoryName || tags.length) {\n // Entry meta block (AGL-582): \"date · category\" line + tag chips.\n entries.push([\n id('meta'),\n {\n $id: id('meta'),\n componentId: Aglyn.COLLECTION_ENTRY_META_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n date,\n category: categoryName,\n tags: tags.join(', '),\n },\n },\n ])\n }\n // The cover, through the ONE shared resolver (AGL-1407). A `media:`\n // reference becomes the site-relative CDN path; a raw storage URL, an\n // AGL-175 CDN path and an author's own hotlinked URL all pass through\n // untouched, which is the documented precedence in `media-ref.ts`.\n const coverImage = Aglyn.resolveMediaSrc(entry.coverImage, { hostId })\n const coverRenders = Boolean(\n coverImage && RENDERABLE_COVER_URL.test(coverImage),\n )\n // A featured video takes the cover's slot, with the cover as its poster\n // (AGL-2956); see `featuredVideoProps` for when an entry has one to play.\n const video = featuredVideoProps(entry, {\n hostId,\n posterRenders: coverRenders,\n })\n if (video) {\n entries.push([\n id('video'),\n {\n $id: id('video'),\n componentId: Aglyn.VIDEO_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: video,\n },\n ])\n } else if (coverRenders) {\n // Rendered as a background image on a plain stack, NOT through the\n // first-party `image` component — that component crashes tenant SSR\n // (AGL-579); the background-image approach is proven safe.\n entries.push([\n id('cover'),\n {\n $id: id('cover'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n role: 'img',\n 'aria-label': entry.title ?? '',\n sx: {\n backgroundImage: `url(\"${cssUrlValue(coverImage)}\")`,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n borderRadius: 1,\n minHeight: 320,\n },\n },\n },\n ])\n }\n entries.push([\n id('body'),\n {\n $id: id('body'),\n componentId: Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { markdown: entry.body ?? '' },\n },\n ])\n // Related posts after the body, share bar at the end (AGL-582); the\n // compose pipeline stamps the related entries server-side.\n entries.push([\n id('related'),\n {\n $id: id('related'),\n componentId: Aglyn.COLLECTION_RELATED_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { heading: 'Related articles', limit: 3, sx: { paddingTop: 2 } },\n },\n ])\n entries.push([\n id('share'),\n {\n $id: id('share'),\n componentId: Aglyn.COLLECTION_SHARE_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {},\n },\n ])\n entries.push([\n id('back'),\n {\n $id: id('back'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n href: `/${collection.slug}`,\n children: `← ${collection.displayName}`,\n sx: { alignSelf: 'flex-start' },\n },\n },\n ])\n return {\n ...shell(\n entries.map(([entryId]) => entryId),\n entryShell(),\n ),\n ...Object.fromEntries(entries),\n }\n}\n\n/** Pagination state for the built-in list (AGL-620). */\nexport interface FallbackListPagination {\n page: number\n perPage: number\n /** @deprecated AGL-3219 — absent past one read's bound. */\n totalPages?: number\n /** The older page's cursor, or `''`/absent when there is none (AGL-3219). */\n nextCursor?: string\n /** The newer page's cursor. */\n prevCursor?: string\n}\n\n/** The routed category for a filtered listing (AGL-1321). */\nexport interface FallbackListCategory {\n slug: string\n name: string\n}\n\n/**\n * Prev/next nav over the cursor addresses (AGL-620, AGL-3219).\n *\n * The label says \"Page 4 of 9\" only where nine is a number that can be known\n * — a collection small enough to be read in one go. Past that it says \"Page\n * 4\" and stops, because the alternative is a total derived from a count the\n * listing's own reads could disagree with, which is how page 10 of a\n * seventeen-page changelog came to call itself the last one.\n */\nfunction paginationNodes(\n collection: FallbackCollection,\n pagination: FallbackListPagination,\n category?: FallbackListCategory,\n): { childId: string; nodes: NodesMap } {\n // Page 1 lives at the bare listing; deeper pages at .../page/{n}. Built\n // through the shared pager computation so this fallback and the\n // `{{pagination.*}}` tokens an authored template binds (AGL-1386) can never\n // disagree — and so the pager stays inside the category it is paging\n // (AGL-1321): a \"next\" that dropped the filter would silently return the\n // reader to the unfiltered list. An edge is the empty string, which is\n // exactly the \"no link here\" this fallback already meant.\n const { page, totalPages, prevUrl, nextUrl } = Aglyn.collectionPaginationLinks(\n {\n collectionSlug: collection.slug,\n ...(category ? { categorySlug: category.slug } : {}),\n page: pagination.page,\n ...(pagination.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n },\n )\n const children: string[] = []\n const nodes: NodesMap = {}\n if (prevUrl) {\n nodes[id('prev')] = {\n $id: id('prev'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: prevUrl, children: '← Newer' },\n }\n children.push(id('prev'))\n }\n nodes[id('pageinfo')] = {\n $id: id('pageinfo'),\n componentId: 'muiTypography',\n pluginId: 'mui',\n parentId: id('pager'),\n props: {\n variant: 'body2',\n children: totalPages > 1 ? `Page ${page} of ${totalPages}` : `Page ${page}`,\n sx: { color: 'text.secondary' },\n },\n }\n children.push(id('pageinfo'))\n if (nextUrl) {\n nodes[id('next')] = {\n $id: id('next'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('pager'),\n props: { href: nextUrl, children: 'Older →' },\n }\n children.push(id('next'))\n }\n nodes[id('pager')] = {\n $id: id('pager'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n direction: 'row',\n spacing: 3,\n sx: { alignItems: 'center', justifyContent: 'center' },\n },\n nodes: children,\n }\n return { childId: id('pager'), nodes }\n}\n\n/**\n * Built-in entry list as canvas nodes (AGL-551): a heading plus a\n * Collection entries block whose template (title, date, excerpt, Read\n * more) the compose pipeline expands over the published entries — the same\n * block designers drop onto their own list-template screens. With\n * `pagination` (AGL-620) the block renders one page and prev/next nav links\n * to `/{slug}/page/{n}`.\n */\nexport function buildCollectionListFallbackNodes(\n collection: FallbackCollection,\n hasEntries: boolean,\n pagination?: FallbackListPagination,\n category?: FallbackListCategory,\n): NodesMap {\n const [titleId, title] = typography('title', {\n variant: 'h3',\n component: 'h1',\n // A filtered listing says which slice it is (AGL-1321) — a page of\n // Guides whose only heading reads \"Blog\" tells the reader nothing about\n // why the other posts vanished.\n children: category\n ? `${collection.displayName} · ${category.name}`\n : collection.displayName,\n })\n // Category pills (AGL-1321): stamped by `expandCollectionCategories` during\n // compose, from the same taxonomy this node's collection carries. Present\n // in the EMPTY case too — a filtered listing with no entries whose pills\n // vanished would be a dead end with no way back to \"All\".\n const pills = collection.categories?.length\n ? {\n $id: id('pills'),\n componentId: Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: { allLabel: 'All' },\n }\n : null\n const lead = pills ? [titleId, id('pills')] : [titleId]\n if (!hasEntries) {\n const [emptyId, empty] = typography('empty', {\n variant: 'body1',\n children: category\n ? `Nothing published in ${category.name} yet.`\n : 'Nothing published yet.',\n sx: { color: 'text.secondary' },\n })\n return {\n ...shell([...lead, emptyId], listShell()),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [emptyId]: empty,\n }\n }\n const item = (suffix: string, props: Record<string, unknown>) => ({\n $id: id(suffix),\n componentId: 'muiTypography' as const,\n pluginId: 'mui',\n parentId: id('item'),\n props,\n })\n /*\n * A pager exists when there is somewhere to go (AGL-3219): an older page,\n * proved by the cursor probe, or a newer one, proved by being past the\n * first. `totalPages > 1` used to stand in for that and could not, once the\n * total stopped being knowable.\n */\n const pager =\n pagination &&\n (Boolean(pagination.nextCursor) ||\n pagination.page > 1 ||\n (pagination.totalPages ?? 0) > 1)\n ? paginationNodes(collection, pagination, category)\n : null\n return {\n ...shell(\n pager\n ? [...lead, id('entries'), pager.childId]\n : [...lead, id('entries')],\n listShell(),\n ),\n [titleId]: title,\n ...(pills ? { [id('pills')]: pills } : {}),\n [id('entries')]: {\n $id: id('entries'),\n componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,\n pluginId: 'mui',\n parentId: id('stack'),\n props: {\n spacing: 4,\n ...(pagination\n ? { perPage: pagination.perPage, page: pagination.page }\n : {}),\n },\n nodes: [id('item')],\n },\n ...(pager ? pager.nodes : {}),\n [id('item')]: {\n $id: id('item'),\n componentId: 'muiStack',\n pluginId: 'mui',\n parentId: id('entries'),\n props: { spacing: 0.5 },\n nodes: [\n id('item-title'),\n id('item-date'),\n id('item-excerpt'),\n id('item-link'),\n ],\n },\n [id('item-title')]: item('item-title', {\n variant: 'h5',\n component: 'h2',\n children: '{{entry.title}}',\n }),\n [id('item-date')]: item('item-date', {\n variant: 'caption',\n children: '{{entry.date}}',\n sx: { color: 'text.secondary' },\n }),\n [id('item-excerpt')]: item('item-excerpt', {\n variant: 'body1',\n children: '{{entry.excerpt}}',\n }),\n [id('item-link')]: {\n $id: id('item-link'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: id('item'),\n props: {\n href: '{{entry.url}}',\n children: 'Read more',\n size: 'small',\n sx: { alignSelf: 'flex-start' },\n },\n },\n }\n}\n\n/** Entry vs list fallback selection for the routed content (AGL-551). */\nexport function buildCollectionFallbackNodes(content: {\n collection: FallbackCollection\n entries: FallbackEntry[]\n entry: FallbackEntry | null\n pagination?: FallbackListPagination | null\n category?: FallbackListCategory | null\n /** The composing site, for host-qualifying a media reference (AGL-1407). */\n hostId?: string\n}): NodesMap {\n return content.entry\n ? buildCollectionEntryFallbackNodes(\n content.collection,\n content.entry,\n content.hostId,\n )\n : buildCollectionListFallbackNodes(\n content.collection,\n content.entries.length > 0,\n content.pagination ?? undefined,\n content.category ?? undefined,\n )\n}\n\nexport default buildCollectionFallbackNodes\n"],"names":["wistiaMediaId","Aglyn","FALLBACK_ID_PREFIX","id","suffix","formatDate","value","formatCollectionEntryDate","RENDERABLE_COVER_URL","cssUrlValue","url","replace","char","encodeURIComponent","ENTRY_MAX_WIDTH","LIST_MAX_WIDTH","LIST_SECTION_GAP","entryShell","maxWidth","stackProps","spacing","listShell","sx","display","flexDirection","gap","shell","childIds","layout","NODE_ROOT_ID","$id","componentId","nodes","pluginId","parentId","props","paddingTop","paddingBottom","typography","featuredVideoProps","entry","options","src","coverVideo","trim","resolveMediaSrc","hostId","undefined","poster","posterRenders","coverImage","description","seoDescription","excerpt","uploadDate","collectionEntryPublishedAtIso","publishedAt","title","width","buildCollectionEntryFallbackNodes","collection","entries","variant","component","children","date","tags","filter","Boolean","categoryName","resolveEntryCategoryName","categories","length","push","COLLECTION_ENTRY_META_COMPONENT_ID","category","join","coverRenders","test","video","VIDEO_COMPONENT_ID","role","backgroundImage","backgroundSize","backgroundPosition","borderRadius","minHeight","COLLECTION_ENTRY_BODY_COMPONENT_ID","markdown","body","COLLECTION_RELATED_COMPONENT_ID","heading","limit","COLLECTION_SHARE_COMPONENT_ID","href","slug","displayName","alignSelf","map","entryId","Object","fromEntries","paginationNodes","pagination","page","totalPages","prevUrl","nextUrl","collectionPaginationLinks","collectionSlug","categorySlug","nextCursor","prevCursor","color","direction","alignItems","justifyContent","childId","buildCollectionListFallbackNodes","hasEntries","titleId","name","pills","COLLECTION_CATEGORIES_COMPONENT_ID","allLabel","lead","emptyId","empty","item","pager","COLLECTION_ENTRIES_COMPONENT_ID","perPage","size","buildCollectionFallbackNodes","content"],"mappings":"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"}
@@ -53,7 +53,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
53
53
  * This SURFACES what the platform already computes — the built-in fallback
54
54
  * pager reads the same function, so the two cannot drift.
55
55
  */ export function collectionTokens(collection, category, pagination) {
56
- var _ref, _ref1;
56
+ var _pagination_nextCursor, _pagination_prevCursor, _ref, _ref1;
57
57
  // An unpaginated listing is page 1 of 1 — both URLs empty, which reads as
58
58
  // the honest "nowhere to page to" rather than a broken link.
59
59
  const pager = Aglyn.collectionPaginationLinks(_extends({
@@ -61,9 +61,13 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
61
61
  }, (category == null ? void 0 : category.slug) ? {
62
62
  categorySlug: category.slug
63
63
  } : {}, {
64
- page: pagination == null ? void 0 : pagination.page,
65
- totalPages: pagination == null ? void 0 : pagination.totalPages
66
- }));
64
+ page: pagination == null ? void 0 : pagination.page
65
+ }, (pagination == null ? void 0 : pagination.totalPages) === undefined ? {} : {
66
+ totalPages: pagination.totalPages
67
+ }, pagination ? {
68
+ nextCursor: (_pagination_nextCursor = pagination.nextCursor) != null ? _pagination_nextCursor : '',
69
+ prevCursor: (_pagination_prevCursor = pagination.prevCursor) != null ? _pagination_prevCursor : ''
70
+ } : {}));
67
71
  return {
68
72
  'collection.name': collection.displayName,
69
73
  'collection.slug': collection.slug,
@@ -85,7 +89,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
85
89
  * falls through to the designed built-in fallback.
86
90
  */ export async function composeCollectionTemplatePage(options) {
87
91
  var _templateRes_screen_seo;
88
- var _templateRes_screen_seo1, _options_host_seo, _options_host, _content_pagination;
92
+ var _templateRes_screen_seo1, _options_host_seo, _options_host, _content_pagination, _content_pagination1;
89
93
  const { hostId, content } = options;
90
94
  const collection = content.collection;
91
95
  if (!collection) return null;
@@ -143,6 +147,8 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
143
147
  categorySlug: content.category.slug
144
148
  } : {}, content.entriesReachedBound ? {
145
149
  entriesReachedBound: true
150
+ } : {}, ((_content_pagination1 = content.pagination) == null ? void 0 : _content_pagination1.windowStart) ? {
151
+ windowStart: content.pagination.windowStart
146
152
  } : {})
147
153
  });
148
154
  if (!nodes) return null;
@@ -188,7 +194,7 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
188
194
  const collection = content.collection;
189
195
  if (!collection) return null;
190
196
  try {
191
- var _content_entry, _host_seo, _content_pagination;
197
+ var _content_entry, _host_seo, _content_pagination, _content_pagination1;
192
198
  // The layout for a page the platform composed rather than the author
193
199
  // (AGL-2513). Still the home screen's layout by default — the rule this
194
200
  // branch has always followed — but the host can now name a different one,
@@ -241,6 +247,8 @@ import { collectSocialImageFacts } from "./social-image-facts.js";
241
247
  categorySlug: content.category.slug
242
248
  } : {}, content.entriesReachedBound ? {
243
249
  entriesReachedBound: true
250
+ } : {}, ((_content_pagination1 = content.pagination) == null ? void 0 : _content_pagination1.windowStart) ? {
251
+ windowStart: content.pagination.windowStart
244
252
  } : {})
245
253
  });
246
254
  return nodes ? _extends({
@@ -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 totalPages: pagination?.totalPages,\n })\n return {\n 'collection.name': collection.displayName,\n 'collection.slug': collection.slug,\n 'collection.category': category?.name ?? '',\n 'collection.categorySlug': category?.slug ?? '',\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n }\n}\n\nexport interface ComposedCollectionPage {\n /** Template screen doc with the entry/collection SEO merged in. */\n screen: Record<string, any>\n nodes: Record<string, any>\n /**\n * The current pair of each asset the head's card may name (AGL-2850): the\n * entry's cover, the template's image, the site default. Read in the\n * composition's batch, and absent when it answered for none of them.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/**\n * Renders a collection route through its designated template screen\n * (AGL-551), the same mechanism as commerce PDP/collection templates: the\n * screen composes through the NORMAL published pipeline — theme, shared\n * layout, reusable components — with `{{entry.*}}`/`{{collection.*}}`\n * tokens substituted and Collection entries blocks expanded. Returns null\n * when no template is designated (or it fails to compose) so the caller\n * falls through to the designed built-in fallback.\n */\nexport async function composeCollectionTemplatePage(options: {\n hostId: string\n content: CollectionContent\n /**\n * The site, whose default card the head falls back to after the entry's\n * cover and the template's image (AGL-2850). Its document is read in this\n * page's batch with theirs.\n */\n host?: Aglyn.AglynHost | null\n}): Promise<ComposedCollectionPage | null> {\n const { hostId, content } = options\n const collection = content.collection\n if (!collection) return null\n const kind = content.entry ? 'entry' : 'list'\n const screenId = resolveCollectionTemplateScreenId(collection, kind)\n if (!screenId) return null\n\n // The one read that WANTS a template document (AGL-1400): this is the\n // composition it exists for, with `{{entry.*}}` substituted against the\n // routed entry. Every path-resolving caller leaves the flag off and gets the\n // 404 a template deserves at an address of its own.\n const templateRes = await getScreen({ hostId, screenId, allowTemplate: true })\n if (!templateRes.screen) return null\n\n const entry = content.entry\n const tokens = entry\n ? {\n ...collectionTokens(collection),\n // Category names resolve against the collection's taxonomy\n // (AGL-582): `categoryId` lookup first, legacy string fallback.\n ...Aglyn.collectionEntryTokens(\n entry,\n collection.slug,\n collection.categories,\n ),\n }\n : collectionTokens(collection, content.category, content.pagination)\n // The head's card on this page, in its order: the entry's cover, the\n // template's own image, then the site default (AGL-2850).\n const card = collectSocialImageFacts([\n entry?.coverImage,\n (templateRes.screen as any).seo?.image,\n options.host?.seo?.image,\n ])\n const nodes = await composeScreenNodes({\n hostId,\n screenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The site the template renders for, so its host variables — and its\n // layout's — fill in as they do on every other page (AGL-2883).\n host: options.host,\n tokens,\n // List pages hand their already-fetched entries to the Collection\n // entries block; entry pages carry the routed entry (AGL-582, Related\n // posts) and let blocks fetch entry lists on demand (e.g. a \"More\n // posts\" section on the article template). The category taxonomy\n // rides along so `{{entry.category}}` resolves inside the blocks.\n collection: entry\n ? { slug: collection.slug, entry, categories: collection.categories }\n : {\n slug: collection.slug,\n // Already filtered to the routed category (AGL-1321) — the block\n // repeats what the ROUTE resolved, so a designer-pinned\n // `filterCategory` on the block narrows it further rather than\n // fighting it.\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category ? { categorySlug: content.category.slug } : {}),\n // The read's own bound (AGL-1516), which has to travel WITH the\n // filtered entries above: once `content.entries` has been narrowed\n // to a category, nothing downstream can tell a complete read from a\n // truncated one by counting it.\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n },\n })\n if (!nodes) return null\n\n const screenSeo = (templateRes.screen as any).seo ?? {}\n const seo = entry\n ? // Entry metadata drives the head (AGL-117 merge; AGL-582 overrides).\n {\n ...screenSeo,\n title: entry.seoTitle || entry.title,\n description: entry.seoDescription || entry.excerpt || undefined,\n // The image and its companions move as ONE group (AGL-2417). The\n // spread above carries the SCREEN's `imageWidth`/`imageHeight`/\n // `imageAlt`, so an entry that supplies its own cover was describing\n // it with the screen default's size and — once alts existed — with\n // the screen default's DESCRIPTION: a sentence about a picture this\n // card does not show, delivered to the reader least able to check.\n // When the entry wins, its own companions win with it.\n ...(entry.coverImage\n ? {\n image: entry.coverImage,\n imageWidth: undefined,\n imageHeight: undefined,\n imageAlt: entry.coverImageAlt || undefined,\n }\n : { image: screenSeo.image || undefined }),\n }\n : // A LIST passes its screen's own SEO through UNTOUCHED (AGL-1345).\n //\n // This used to default `title` to `collection.displayName`, which reads\n // like a harmless fallback and is not: it made an authored title\n // indistinguishable from a generated one by the time the head was built.\n // The title rule (AGL-1341) turns on exactly that distinction — an\n // authored title renders VERBATIM, a name joins the site title — so a\n // consumer reading this could only choose between dropping the site\n // title off every untitled list (\"Changelog\") or ignoring the author's\n // title on every titled one (\"Changelog – Acme\" over the sentence they\n // wrote). The collection name is still the fallback; it just belongs to\n // the title resolver, as the page's `name`, alongside every other\n // surface's fallback rather than baked into stored SEO here.\n screenSeo\n return {\n screen: { ...(templateRes.screen as any), seo },\n nodes,\n ...card.collected(),\n }\n}\n\n/**\n * The designed built-in rendering (AGL-551): when a collection has no\n * template screen, its routes still compose through the site's theme and\n * the host's default shared layout (the home screen's layout) instead of\n * the old unthemed article. Fail-open — any error returns null and the\n * caller keeps the legacy plain rendering.\n */\nexport async function composeCollectionFallbackPage(options: {\n hostId: string\n host: Aglyn.AglynHost\n content: CollectionContent\n}): Promise<Omit<ComposedCollectionPage, 'screen'> | null> {\n const { hostId, host, content } = options\n const collection = content.collection\n if (!collection) return null\n try {\n // The layout for a page the platform composed rather than the author\n // (AGL-2513). Still the home screen's layout by default — the rule this\n // branch has always followed — but the host can now name a different one,\n // and site search reads the same setting so the two built-in pages of a\n // site cannot end up in different chrome.\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const screenNodes = buildCollectionFallbackNodes({\n collection,\n entries: content.entries,\n entry: content.entry,\n pagination: content.pagination,\n category: content.category,\n // The cover resolves through `resolveMediaSrc` (AGL-1407), and an\n // org-scoped reference has to name the site asking or a site-restricted\n // asset will not serve.\n hostId,\n })\n // The head's card on a page with no template: the entry's cover, then the\n // site default (AGL-2850).\n const card = collectSocialImageFacts([\n content.entry?.coverImage,\n host?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes,\n socialImages: card.socialImages,\n // The layout's host variables fill in from this site (AGL-2883).\n host,\n // Entry routes resolve with an EMPTY entries list (the loader only\n // fetched the one entry), so hand the routed entry over and let the\n // Related posts block fetch the list on demand (AGL-582); list\n // routes keep their already-fetched entries. Categories ride along\n // for `categoryId` → name resolution.\n collection: content.entry\n ? {\n slug: collection.slug,\n entry: content.entry,\n categories: collection.categories,\n }\n : {\n slug: collection.slug,\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category\n ? { categorySlug: content.category.slug }\n : {}),\n // Same fact, same reason as the template path above (AGL-1516).\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n },\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","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","screenSeo","title","seoTitle","description","seoDescription","excerpt","imageWidth","imageHeight","imageAlt","coverImageAlt","collected","composeCollectionFallbackPage","layoutId","screenNodes","error","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-collection-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport buildCollectionFallbackNodes from './collection-fallback-nodes'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from './compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport type { CollectionContent } from './get-collection-content'\nimport getScreen from './get-screen'\nimport { collectSocialImageFacts } from './social-image-facts'\n\ntype CollectionDoc = NonNullable<CollectionContent['collection']>\n\n/**\n * Which template screen a collection route renders through (AGL-551):\n * `/{collection}` uses `listScreenId`, `/{collection}/{entry}` uses\n * `entryScreenId` — falling back to the legacy AGL-105 `templateScreenId`\n * so existing blogs keep rendering. `undefined` means no template is set\n * and the designed built-in fallback applies.\n */\nexport function resolveCollectionTemplateScreenId(\n collection: Pick<\n CollectionDoc,\n 'listScreenId' | 'entryScreenId' | 'templateScreenId'\n >,\n kind: 'list' | 'entry',\n): string | undefined {\n if (kind === 'list') return collection.listScreenId || undefined\n return collection.entryScreenId || collection.templateScreenId || undefined\n}\n\n/**\n * Page-level `{{collection.*}}` tokens for template screens (AGL-551), plus\n * the routed category (AGL-1321) so a list template can name what it is\n * showing — \"Guides\" rather than a heading that says \"Blog\" on every filtered\n * URL. Both category tokens resolve to the empty string on the unfiltered\n * listing, which is what makes them safe to bind unconditionally.\n *\n * `{{pagination.*}}` follows the same design (AGL-1386). One static list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, so a hand-built pager on it renders identically on all\n * of them: \"Older →\" on a category that has one page, pointing at a URL that\n * dropped the filter. The URLs come from `collectionPaginationLinks`, which\n * builds them through the shared listing-URL builder (so they carry the\n * category) and resolves the EDGES TO THE EMPTY STRING — no previous page,\n * no `prevUrl`. There is no runtime conditional to hide a link with (the\n * `condition` field on nodes is editor-side field visibility, not a render\n * gate), so the empty string is what makes an unconditional binding correct\n * on every route: a link whose href does not resolve renders as an inert\n * placeholder of the same element (AGL-1268/1357).\n *\n * This SURFACES what the platform already computes — the built-in fallback\n * pager reads the same function, so the two cannot drift.\n */\nexport function collectionTokens(\n collection: Pick<CollectionDoc, 'displayName' | 'slug'>,\n category?: CollectionContent['category'],\n pagination?: CollectionContent['pagination'],\n): Record<string, string> {\n // An unpaginated listing is page 1 of 1 — both URLs empty, which reads as\n // the honest \"nowhere to page to\" rather than a broken link.\n const pager = Aglyn.collectionPaginationLinks({\n collectionSlug: collection.slug,\n ...(category?.slug ? { categorySlug: category.slug } : {}),\n page: pagination?.page,\n ...(pagination?.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n // Only a LISTING has cursors, so passing them is also what tells the\n // builder this is a listing and not an inert entry route (AGL-3219).\n ...(pagination\n ? {\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n }\n : {}),\n })\n return {\n 'collection.name': collection.displayName,\n 'collection.slug': collection.slug,\n 'collection.category': category?.name ?? '',\n 'collection.categorySlug': category?.slug ?? '',\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n }\n}\n\nexport interface ComposedCollectionPage {\n /** Template screen doc with the entry/collection SEO merged in. */\n screen: Record<string, any>\n nodes: Record<string, any>\n /**\n * The current pair of each asset the head's card may name (AGL-2850): the\n * entry's cover, the template's image, the site default. Read in the\n * composition's batch, and absent when it answered for none of them.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/**\n * Renders a collection route through its designated template screen\n * (AGL-551), the same mechanism as commerce PDP/collection templates: the\n * screen composes through the NORMAL published pipeline — theme, shared\n * layout, reusable components — with `{{entry.*}}`/`{{collection.*}}`\n * tokens substituted and Collection entries blocks expanded. Returns null\n * when no template is designated (or it fails to compose) so the caller\n * falls through to the designed built-in fallback.\n */\nexport async function composeCollectionTemplatePage(options: {\n hostId: string\n content: CollectionContent\n /**\n * The site, whose default card the head falls back to after the entry's\n * cover and the template's image (AGL-2850). Its document is read in this\n * page's batch with theirs.\n */\n host?: Aglyn.AglynHost | null\n}): Promise<ComposedCollectionPage | null> {\n const { hostId, content } = options\n const collection = content.collection\n if (!collection) return null\n const kind = content.entry ? 'entry' : 'list'\n const screenId = resolveCollectionTemplateScreenId(collection, kind)\n if (!screenId) return null\n\n // The one read that WANTS a template document (AGL-1400): this is the\n // composition it exists for, with `{{entry.*}}` substituted against the\n // routed entry. Every path-resolving caller leaves the flag off and gets the\n // 404 a template deserves at an address of its own.\n const templateRes = await getScreen({ hostId, screenId, allowTemplate: true })\n if (!templateRes.screen) return null\n\n const entry = content.entry\n const tokens = entry\n ? {\n ...collectionTokens(collection),\n // Category names resolve against the collection's taxonomy\n // (AGL-582): `categoryId` lookup first, legacy string fallback.\n ...Aglyn.collectionEntryTokens(\n entry,\n collection.slug,\n collection.categories,\n ),\n }\n : collectionTokens(collection, content.category, content.pagination)\n // The head's card on this page, in its order: the entry's cover, the\n // template's own image, then the site default (AGL-2850).\n const card = collectSocialImageFacts([\n entry?.coverImage,\n (templateRes.screen as any).seo?.image,\n options.host?.seo?.image,\n ])\n const nodes = await composeScreenNodes({\n hostId,\n screenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The site the template renders for, so its host variables — and its\n // layout's — fill in as they do on every other page (AGL-2883).\n host: options.host,\n tokens,\n // List pages hand their already-fetched entries to the Collection\n // entries block; entry pages carry the routed entry (AGL-582, Related\n // posts) and let blocks fetch entry lists on demand (e.g. a \"More\n // posts\" section on the article template). The category taxonomy\n // rides along so `{{entry.category}}` resolves inside the blocks.\n collection: entry\n ? { slug: collection.slug, entry, categories: collection.categories }\n : {\n slug: collection.slug,\n // Already filtered to the routed category (AGL-1321) — the block\n // repeats what the ROUTE resolved, so a designer-pinned\n // `filterCategory` on the block narrows it further rather than\n // fighting it.\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category ? { categorySlug: content.category.slug } : {}),\n // The read's own bound (AGL-1516), which has to travel WITH the\n // filtered entries above: once `content.entries` has been narrowed\n // to a category, nothing downstream can tell a complete read from a\n // truncated one by counting it.\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n // Where those entries start (AGL-3213): a listing page past the\n // cached read's bound holds its own window, not the collection\n // from the top, and the block has to window from the same origin.\n ...(content.pagination?.windowStart\n ? { windowStart: content.pagination.windowStart }\n : {}),\n },\n })\n if (!nodes) return null\n\n const screenSeo = (templateRes.screen as any).seo ?? {}\n const seo = entry\n ? // Entry metadata drives the head (AGL-117 merge; AGL-582 overrides).\n {\n ...screenSeo,\n title: entry.seoTitle || entry.title,\n description: entry.seoDescription || entry.excerpt || undefined,\n // The image and its companions move as ONE group (AGL-2417). The\n // spread above carries the SCREEN's `imageWidth`/`imageHeight`/\n // `imageAlt`, so an entry that supplies its own cover was describing\n // it with the screen default's size and — once alts existed — with\n // the screen default's DESCRIPTION: a sentence about a picture this\n // card does not show, delivered to the reader least able to check.\n // When the entry wins, its own companions win with it.\n ...(entry.coverImage\n ? {\n image: entry.coverImage,\n imageWidth: undefined,\n imageHeight: undefined,\n imageAlt: entry.coverImageAlt || undefined,\n }\n : { image: screenSeo.image || undefined }),\n }\n : // A LIST passes its screen's own SEO through UNTOUCHED (AGL-1345).\n //\n // This used to default `title` to `collection.displayName`, which reads\n // like a harmless fallback and is not: it made an authored title\n // indistinguishable from a generated one by the time the head was built.\n // The title rule (AGL-1341) turns on exactly that distinction — an\n // authored title renders VERBATIM, a name joins the site title — so a\n // consumer reading this could only choose between dropping the site\n // title off every untitled list (\"Changelog\") or ignoring the author's\n // title on every titled one (\"Changelog – Acme\" over the sentence they\n // wrote). The collection name is still the fallback; it just belongs to\n // the title resolver, as the page's `name`, alongside every other\n // surface's fallback rather than baked into stored SEO here.\n screenSeo\n return {\n screen: { ...(templateRes.screen as any), seo },\n nodes,\n ...card.collected(),\n }\n}\n\n/**\n * The designed built-in rendering (AGL-551): when a collection has no\n * template screen, its routes still compose through the site's theme and\n * the host's default shared layout (the home screen's layout) instead of\n * the old unthemed article. Fail-open — any error returns null and the\n * caller keeps the legacy plain rendering.\n */\nexport async function composeCollectionFallbackPage(options: {\n hostId: string\n host: Aglyn.AglynHost\n content: CollectionContent\n}): Promise<Omit<ComposedCollectionPage, 'screen'> | null> {\n const { hostId, host, content } = options\n const collection = content.collection\n if (!collection) return null\n try {\n // The layout for a page the platform composed rather than the author\n // (AGL-2513). Still the home screen's layout by default — the rule this\n // branch has always followed — but the host can now name a different one,\n // and site search reads the same setting so the two built-in pages of a\n // site cannot end up in different chrome.\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host })\n const screenNodes = buildCollectionFallbackNodes({\n collection,\n entries: content.entries,\n entry: content.entry,\n pagination: content.pagination,\n category: content.category,\n // The cover resolves through `resolveMediaSrc` (AGL-1407), and an\n // org-scoped reference has to name the site asking or a site-restricted\n // asset will not serve.\n hostId,\n })\n // The head's card on a page with no template: the entry's cover, then the\n // site default (AGL-2850).\n const card = collectSocialImageFacts([\n content.entry?.coverImage,\n host?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes,\n socialImages: card.socialImages,\n // The layout's host variables fill in from this site (AGL-2883).\n host,\n // Entry routes resolve with an EMPTY entries list (the loader only\n // fetched the one entry), so hand the routed entry over and let the\n // Related posts block fetch the list on demand (AGL-582); list\n // routes keep their already-fetched entries. Categories ride along\n // for `categoryId` → name resolution.\n collection: content.entry\n ? {\n slug: collection.slug,\n entry: content.entry,\n categories: collection.categories,\n }\n : {\n slug: collection.slug,\n entries: content.entries,\n categories: collection.categories,\n ...(content.pagination?.page\n ? { page: content.pagination.page }\n : {}),\n ...(content.category\n ? { categorySlug: content.category.slug }\n : {}),\n // Same fact, same reason as the template path above (AGL-1516).\n ...(content.entriesReachedBound\n ? { entriesReachedBound: true }\n : {}),\n // Same window, same reason as the template path above (AGL-3213).\n ...(content.pagination?.windowStart\n ? { windowStart: content.pagination.windowStart }\n : {}),\n },\n })\n return nodes ? { nodes, ...card.collected() } : null\n } catch (error) {\n console.error(error)\n return null\n }\n}\n\nexport default composeCollectionTemplatePage\n"],"names":["Aglyn","buildCollectionFallbackNodes","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","getScreen","collectSocialImageFacts","resolveCollectionTemplateScreenId","collection","kind","listScreenId","undefined","entryScreenId","templateScreenId","collectionTokens","category","pagination","pager","collectionPaginationLinks","collectionSlug","slug","categorySlug","page","totalPages","nextCursor","prevCursor","displayName","name","String","prevUrl","nextUrl","composeCollectionTemplatePage","options","content","hostId","entry","screenId","templateRes","allowTemplate","screen","tokens","collectionEntryTokens","categories","card","coverImage","seo","image","host","nodes","socialImages","entries","entriesReachedBound","windowStart","screenSeo","title","seoTitle","description","seoDescription","excerpt","imageWidth","imageHeight","imageAlt","coverImageAlt","collected","composeCollectionFallbackPage","layoutId","screenNodes","error","console"],"mappings":"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"}
@@ -53,6 +53,15 @@ export interface ComposeCollectionContext {
53
53
  * category filter both in between — `entries.length` no longer tells them.
54
54
  */
55
55
  entriesReachedBound?: boolean;
56
+ /**
57
+ * Where `entries` begins in the collection's own order (AGL-3213).
58
+ *
59
+ * Zero for every listing served from the cached read. A page past that
60
+ * read's bound carries only its own ten entries, and the block windows
61
+ * `[(page - 1) * perPage, …)` — so without this it would slice from an
62
+ * offset its own array never reaches and render an empty listing.
63
+ */
64
+ windowStart?: number;
56
65
  /**
57
66
  * Whether {@link slug} is a cache KEY rather than an address (AGL-2524).
58
67
  *
@@ -150,6 +150,8 @@ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
150
150
  page: collection.page
151
151
  } : {}, collection.entriesReachedBound ? {
152
152
  reachedBound: true
153
+ } : {}, collection.windowStart ? {
154
+ windowStart: collection.windowStart
153
155
  } : {});
154
156
  return;
155
157
  }