@aglyn/tenant-runtime 1.0.0-beta.147 → 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.147",
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.147",
29
- "@aglyn/tenant-data-admin": "1.0.0-beta.147",
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,
@@ -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 // 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","displayName","name","String","prevUrl","nextUrl","composeCollectionTemplatePage","options","content","hostId","entry","screenId","templateRes","allowTemplate","screen","tokens","collectionEntryTokens","categories","card","coverImage","seo","image","host","nodes","socialImages","entries","entriesReachedBound","windowStart","screenSeo","title","seoTitle","description","seoDescription","excerpt","imageWidth","imageHeight","imageAlt","coverImageAlt","collected","composeCollectionFallbackPage","layoutId","screenNodes","error","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-collection-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport buildCollectionFallbackNodes from './collection-fallback-nodes'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from './compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from './built-in-page-layout'\nimport type { CollectionContent } from './get-collection-content'\nimport getScreen from './get-screen'\nimport { collectSocialImageFacts } from './social-image-facts'\n\ntype CollectionDoc = NonNullable<CollectionContent['collection']>\n\n/**\n * Which template screen a collection route renders through (AGL-551):\n * `/{collection}` uses `listScreenId`, `/{collection}/{entry}` uses\n * `entryScreenId` — falling back to the legacy AGL-105 `templateScreenId`\n * so existing blogs keep rendering. `undefined` means no template is set\n * and the designed built-in fallback applies.\n */\nexport function resolveCollectionTemplateScreenId(\n collection: Pick<\n CollectionDoc,\n 'listScreenId' | 'entryScreenId' | 'templateScreenId'\n >,\n kind: 'list' | 'entry',\n): string | undefined {\n if (kind === 'list') return collection.listScreenId || undefined\n return collection.entryScreenId || collection.templateScreenId || undefined\n}\n\n/**\n * Page-level `{{collection.*}}` tokens for template screens (AGL-551), plus\n * the routed category (AGL-1321) so a list template can name what it is\n * showing — \"Guides\" rather than a heading that says \"Blog\" on every filtered\n * URL. Both category tokens resolve to the empty string on the unfiltered\n * listing, which is what makes them safe to bind unconditionally.\n *\n * `{{pagination.*}}` follows the same design (AGL-1386). One static list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, so a hand-built pager on it renders identically on all\n * of them: \"Older →\" on a category that has one page, pointing at a URL that\n * dropped the filter. The URLs come from `collectionPaginationLinks`, which\n * builds them through the shared listing-URL builder (so they carry the\n * category) and resolves the EDGES TO THE EMPTY STRING — no previous page,\n * no `prevUrl`. There is no runtime conditional to hide a link with (the\n * `condition` field on nodes is editor-side field visibility, not a render\n * gate), so the empty string is what makes an unconditional binding correct\n * on every route: a link whose href does not resolve renders as an inert\n * placeholder of the same element (AGL-1268/1357).\n *\n * This SURFACES what the platform already computes — the built-in fallback\n * pager reads the same function, so the two cannot drift.\n */\nexport function collectionTokens(\n collection: Pick<CollectionDoc, 'displayName' | 'slug'>,\n category?: CollectionContent['category'],\n pagination?: CollectionContent['pagination'],\n): Record<string, string> {\n // An unpaginated listing is page 1 of 1 — both URLs empty, which reads as\n // the honest \"nowhere to page to\" rather than a broken link.\n const pager = Aglyn.collectionPaginationLinks({\n collectionSlug: collection.slug,\n ...(category?.slug ? { categorySlug: category.slug } : {}),\n page: pagination?.page,\n ...(pagination?.totalPages === undefined\n ? {}\n : { totalPages: pagination.totalPages }),\n // Only a LISTING has cursors, so passing them is also what tells the\n // builder this is a listing and not an inert entry route (AGL-3219).\n ...(pagination\n ? {\n nextCursor: pagination.nextCursor ?? '',\n prevCursor: pagination.prevCursor ?? '',\n }\n : {}),\n })\n return {\n 'collection.name': collection.displayName,\n 'collection.slug': collection.slug,\n 'collection.category': category?.name ?? '',\n 'collection.categorySlug': category?.slug ?? '',\n 'pagination.page': String(pager.page),\n 'pagination.totalPages': String(pager.totalPages),\n 'pagination.prevUrl': pager.prevUrl,\n 'pagination.nextUrl': pager.nextUrl,\n }\n}\n\nexport interface ComposedCollectionPage {\n /** Template screen doc with the entry/collection SEO merged in. */\n screen: Record<string, any>\n nodes: Record<string, any>\n /**\n * The current pair of each asset the head's card may name (AGL-2850): the\n * entry's cover, the template's image, the site default. Read in the\n * composition's batch, and absent when it answered for none of them.\n */\n socialImageFacts?: Aglyn.SocialImageAssetFacts\n}\n\n/**\n * Renders a collection route through its designated template screen\n * (AGL-551), the same mechanism as commerce PDP/collection templates: the\n * screen composes through the NORMAL published pipeline — theme, shared\n * layout, reusable components — with `{{entry.*}}`/`{{collection.*}}`\n * tokens substituted and Collection entries blocks expanded. Returns null\n * when no template is designated (or it fails to compose) so the caller\n * falls through to the designed built-in fallback.\n */\nexport async function composeCollectionTemplatePage(options: {\n hostId: string\n 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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,OAAOC,kCAAkC,iCAA6B;AACtE,OAAOC,sBACLC,sBAAsB,QACjB,4BAAwB;AAC/B,SAASC,0BAA0B,QAAQ,4BAAwB;AAEnE,OAAOC,eAAe,kBAAc;AACpC,SAASC,uBAAuB,QAAQ,0BAAsB;AAI9D;;;;;;CAMC,GACD,OAAO,SAASC,kCACdC,UAGC,EACDC,IAAsB;IAEtB,IAAIA,SAAS,QAAQ,OAAOD,WAAWE,YAAY,IAAIC;IACvD,OAAOH,WAAWI,aAAa,IAAIJ,WAAWK,gBAAgB,IAAIF;AACpE;AAEA;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASG,iBACdN,UAAuD,EACvDO,QAAwC,EACxCC,UAA4C;QAexBA,wBACAA;IAdpB,0EAA0E;IAC1E,6DAA6D;IAC7D,MAAMC,QAAQjB,MAAMkB,yBAAyB,CAAC;QAC5CC,gBAAgBX,WAAWY,IAAI;OAC3BL,CAAAA,4BAAAA,SAAUK,IAAI,IAAG;QAAEC,cAAcN,SAASK,IAAI;IAAC,IAAI,CAAC;QACxDE,IAAI,EAAEN,8BAAAA,WAAYM,IAAI;OAClBN,CAAAA,8BAAAA,WAAYO,UAAU,MAAKZ,YAC3B,CAAC,IACD;QAAEY,YAAYP,WAAWO,UAAU;IAAC,GAGpCP,aACA;QACEQ,UAAU,GAAER,yBAAAA,WAAWQ,UAAU,YAArBR,yBAAyB;QACrCS,UAAU,GAAET,yBAAAA,WAAWS,UAAU,YAArBT,yBAAyB;IACvC,IACA,CAAC;IAEP,OAAO;QACL,mBAAmBR,WAAWkB,WAAW;QACzC,mBAAmBlB,WAAWY,IAAI;QAClC,qBAAqB,UAAEL,4BAAAA,SAAUY,IAAI,mBAAI;QACzC,yBAAyB,WAAEZ,4BAAAA,SAAUK,IAAI,oBAAI;QAC7C,mBAAmBQ,OAAOX,MAAMK,IAAI;QACpC,yBAAyBM,OAAOX,MAAMM,UAAU;QAChD,sBAAsBN,MAAMY,OAAO;QACnC,sBAAsBZ,MAAMa,OAAO;IACrC;AACF;AAcA;;;;;;;;CAQC,GACD,OAAO,eAAeC,8BAA8BC,OASnD;QAgFmB;QAhDhB,0BACAA,mBAAAA,eA0BUC,qBAcAA;IAxEZ,MAAM,EAAEC,MAAM,EAAED,OAAO,EAAE,GAAGD;IAC5B,MAAMxB,aAAayB,QAAQzB,UAAU;IACrC,IAAI,CAACA,YAAY,OAAO;IACxB,MAAMC,OAAOwB,QAAQE,KAAK,GAAG,UAAU;IACvC,MAAMC,WAAW7B,kCAAkCC,YAAYC;IAC/D,IAAI,CAAC2B,UAAU,OAAO;IAEtB,sEAAsE;IACtE,wEAAwE;IACxE,6EAA6E;IAC7E,oDAAoD;IACpD,MAAMC,cAAc,MAAMhC,UAAU;QAAE6B;QAAQE;QAAUE,eAAe;IAAK;IAC5E,IAAI,CAACD,YAAYE,MAAM,EAAE,OAAO;IAEhC,MAAMJ,QAAQF,QAAQE,KAAK;IAC3B,MAAMK,SAASL,QACX,aACKrB,iBAAiBN,aAGjBR,MAAMyC,qBAAqB,CAC5BN,OACA3B,WAAWY,IAAI,EACfZ,WAAWkC,UAAU,KAGzB5B,iBAAiBN,YAAYyB,QAAQlB,QAAQ,EAAEkB,QAAQjB,UAAU;IACrE,qEAAqE;IACrE,0DAA0D;IAC1D,MAAM2B,OAAOrC,wBAAwB;QACnC6B,yBAAAA,MAAOS,UAAU;SACjB,2BAAA,AAACP,YAAYE,MAAM,CAASM,GAAG,qBAA/B,yBAAiCC,KAAK;SACtCd,gBAAAA,QAAQe,IAAI,sBAAZf,oBAAAA,cAAca,GAAG,qBAAjBb,kBAAmBc,KAAK;KACzB;IACD,MAAME,QAAQ,MAAM9C,mBAAmB;QACrCgC;QACAE;QACAG,QAAQF,YAAYE,MAAM;QAC1BU,cAAcN,KAAKM,YAAY;QAC/B,qEAAqE;QACrE,gEAAgE;QAChEF,MAAMf,QAAQe,IAAI;QAClBP;QACA,kEAAkE;QAClE,sEAAsE;QACtE,kEAAkE;QAClE,iEAAiE;QACjE,kEAAkE;QAClEhC,YAAY2B,QACR;YAAEf,MAAMZ,WAAWY,IAAI;YAAEe;YAAOO,YAAYlC,WAAWkC,UAAU;QAAC,IAClE;YACEtB,MAAMZ,WAAWY,IAAI;YACrB,iEAAiE;YACjE,wDAAwD;YACxD,+DAA+D;YAC/D,eAAe;YACf8B,SAASjB,QAAQiB,OAAO;YACxBR,YAAYlC,WAAWkC,UAAU;WAC7BT,EAAAA,sBAAAA,QAAQjB,UAAU,qBAAlBiB,oBAAoBX,IAAI,IACxB;YAAEA,MAAMW,QAAQjB,UAAU,CAACM,IAAI;QAAC,IAChC,CAAC,GACDW,QAAQlB,QAAQ,GAAG;YAAEM,cAAcY,QAAQlB,QAAQ,CAACK,IAAI;QAAC,IAAI,CAAC,GAK9Da,QAAQkB,mBAAmB,GAC3B;YAAEA,qBAAqB;QAAK,IAC5B,CAAC,GAIDlB,EAAAA,uBAAAA,QAAQjB,UAAU,qBAAlBiB,qBAAoBmB,WAAW,IAC/B;YAAEA,aAAanB,QAAQjB,UAAU,CAACoC,WAAW;QAAC,IAC9C,CAAC;IAEb;IACA,IAAI,CAACJ,OAAO,OAAO;IAEnB,MAAMK,aAAY,0BAAA,AAAChB,YAAYE,MAAM,CAASM,GAAG,YAA/B,0BAAmC,CAAC;IACtD,MAAMA,MAAMV,QAER,aACKkB;QACHC,OAAOnB,MAAMoB,QAAQ,IAAIpB,MAAMmB,KAAK;QACpCE,aAAarB,MAAMsB,cAAc,IAAItB,MAAMuB,OAAO,IAAI/C;OAQlDwB,MAAMS,UAAU,GAChB;QACEE,OAAOX,MAAMS,UAAU;QACvBe,YAAYhD;QACZiD,aAAajD;QACbkD,UAAU1B,MAAM2B,aAAa,IAAInD;IACnC,IACA;QAAEmC,OAAOO,UAAUP,KAAK,IAAInC;IAAU,KAG5C,EAAE;IACF,wEAAwE;IACxE,iEAAiE;IACjE,yEAAyE;IACzE,mEAAmE;IACnE,sEAAsE;IACtE,oEAAoE;IACpE,uEAAuE;IACvE,uEAAuE;IACvE,wEAAwE;IACxE,kEAAkE;IAClE,6DAA6D;IAC7D0C;IACJ,OAAO;QACLd,QAAQ,aAAMF,YAAYE,MAAM;YAAUM;;QAC1CG;OACGL,KAAKoB,SAAS;AAErB;AAEA;;;;;;CAMC,GACD,OAAO,eAAeC,8BAA8BhC,OAInD;IACC,MAAM,EAAEE,MAAM,EAAEa,IAAI,EAAEd,OAAO,EAAE,GAAGD;IAClC,MAAMxB,aAAayB,QAAQzB,UAAU;IACrC,IAAI,CAACA,YAAY,OAAO;IACxB,IAAI;YAqBAyB,gBACAc,WAwBUd,qBAWAA;QAxDZ,qEAAqE;QACrE,wEAAwE;QACxE,0EAA0E;QAC1E,wEAAwE;QACxE,0CAA0C;QAC1C,MAAMgC,WAAW,MAAM7D,2BAA2B;YAAE8B;YAAQa;QAAK;QACjE,MAAMmB,cAAcjE,6BAA6B;YAC/CO;YACA0C,SAASjB,QAAQiB,OAAO;YACxBf,OAAOF,QAAQE,KAAK;YACpBnB,YAAYiB,QAAQjB,UAAU;YAC9BD,UAAUkB,QAAQlB,QAAQ;YAC1B,kEAAkE;YAClE,wEAAwE;YACxE,wBAAwB;YACxBmB;QACF;QACA,0EAA0E;QAC1E,2BAA2B;QAC3B,MAAMS,OAAOrC,wBAAwB;aACnC2B,iBAAAA,QAAQE,KAAK,qBAAbF,eAAeW,UAAU;YACzBG,yBAAAA,YAAAA,KAAMF,GAAG,qBAATE,UAAWD,KAAK;SACjB;QACD,MAAME,QAAQ,MAAM7C,uBAAuB;YACzC+B;YACA+B;YACAC;YACAjB,cAAcN,KAAKM,YAAY;YAC/B,iEAAiE;YACjEF;YACA,mEAAmE;YACnE,oEAAoE;YACpE,+DAA+D;YAC/D,mEAAmE;YACnE,sCAAsC;YACtCvC,YAAYyB,QAAQE,KAAK,GACrB;gBACEf,MAAMZ,WAAWY,IAAI;gBACrBe,OAAOF,QAAQE,KAAK;gBACpBO,YAAYlC,WAAWkC,UAAU;YACnC,IACA;gBACEtB,MAAMZ,WAAWY,IAAI;gBACrB8B,SAASjB,QAAQiB,OAAO;gBACxBR,YAAYlC,WAAWkC,UAAU;eAC7BT,EAAAA,sBAAAA,QAAQjB,UAAU,qBAAlBiB,oBAAoBX,IAAI,IACxB;gBAAEA,MAAMW,QAAQjB,UAAU,CAACM,IAAI;YAAC,IAChC,CAAC,GACDW,QAAQlB,QAAQ,GAChB;gBAAEM,cAAcY,QAAQlB,QAAQ,CAACK,IAAI;YAAC,IACtC,CAAC,GAEDa,QAAQkB,mBAAmB,GAC3B;gBAAEA,qBAAqB;YAAK,IAC5B,CAAC,GAEDlB,EAAAA,uBAAAA,QAAQjB,UAAU,qBAAlBiB,qBAAoBmB,WAAW,IAC/B;gBAAEA,aAAanB,QAAQjB,UAAU,CAACoC,WAAW;YAAC,IAC9C,CAAC;QAEb;QACA,OAAOJ,QAAQ;YAAEA;WAAUL,KAAKoB,SAAS,MAAO;IAClD,EAAE,OAAOI,OAAO;QACdC,QAAQD,KAAK,CAACA;QACd,OAAO;IACT;AACF;AAEA,eAAepC,8BAA6B"}
@@ -222,11 +222,43 @@ export interface CollectionRouteCategory {
222
222
  known: boolean;
223
223
  }
224
224
  export interface CollectionPagination {
225
- /** 1-based current page. */
225
+ /**
226
+ * 1-based counter for display only (AGL-3219).
227
+ *
228
+ * It is what `{{pagination.page}}` renders and nothing reads it back: a
229
+ * cursor page is addressed by the document it starts after, so this number
230
+ * describes how far a reader has walked rather than where the read began.
231
+ * Hand-edit it in a URL and the label is wrong; the entries are not.
232
+ */
226
233
  page: number;
227
234
  perPage: number;
228
- totalPages: number;
229
- totalEntries: number;
235
+ /**
236
+ * The document id the NEXT (older) page starts after, or `''` when this is
237
+ * the last page (AGL-3219).
238
+ *
239
+ * Set from a `limit(perPage + 1)` probe: the extra row is the only evidence
240
+ * needed that an older page exists, and it costs one document rather than
241
+ * the `count()` aggregation a total used to need. It is also the only
242
+ * honest answer available — see {@link CollectionPagination.totalPages}.
243
+ */
244
+ nextCursor: string;
245
+ /** The document id the PREVIOUS (newer) page starts before, or `''`. */
246
+ prevCursor: string;
247
+ /**
248
+ * How many pages there are.
249
+ *
250
+ * @deprecated AGL-3219. Still resolved, so a template binding
251
+ * `{{pagination.totalPages}}` keeps rendering, but no longer a promise: a
252
+ * total can only be stated for a collection that fits inside one read, and
253
+ * past that it is absent rather than wrong. It used to be derived from a
254
+ * `count()` over a set the listing's two reads disagreed about, which is
255
+ * how the page 10/11 seam came to repeat an entry. Bind
256
+ * {@link CollectionPagination.nextCursor} instead — `''` means there is
257
+ * nothing older, which is the question a pager is actually asking.
258
+ */
259
+ totalPages?: number;
260
+ /** @deprecated AGL-3219, with {@link CollectionPagination.totalPages}. */
261
+ totalEntries?: number;
230
262
  /**
231
263
  * Where `entries` begins in the collection's own order (AGL-3213): 0 for a
232
264
  * listing served from the cached read, and the page's own offset for one
@@ -240,6 +272,27 @@ export interface CollectionPagination {
240
272
  */
241
273
  windowStart?: number;
242
274
  }
275
+ /**
276
+ * The cursor a retired `/{collection}/page/{n}` address redirects onto
277
+ * (AGL-3219).
278
+ *
279
+ * This is the ONE place a position is still resolved, and it is deliberately
280
+ * the only one: a 301 is served once, the reader lands on an address that
281
+ * cannot drift afterwards, and whatever the position meant at that instant is
282
+ * the page they would have got anyway. Everything downstream is cursors.
283
+ *
284
+ * Keys only — `select()` with no fields asks Firestore for document names and
285
+ * nothing else — so the skipped documents are billed at the cheapest rate the
286
+ * offset can be had for. Returns `''` when the position is past the end,
287
+ * which is a 404 rather than a redirect to nowhere.
288
+ */
289
+ export declare function resolveCollectionPageCursor(options: {
290
+ hostId: string;
291
+ collectionSlug: string;
292
+ /** 1-based page whose PREVIOUS entry is the cursor. */
293
+ page: number;
294
+ perPage: number;
295
+ }): Promise<string>;
243
296
  /** Compose-time view of a collection: its live entries and its taxonomy. */
244
297
  export interface PublishedCollectionSource {
245
298
  /**
@@ -271,12 +324,6 @@ export interface PublishedCollectionSource {
271
324
  * would tell a reader their search covered less than it did.
272
325
  */
273
326
  reachedBound: boolean;
274
- /**
275
- * How many entries are live in the whole collection (AGL-3213) — see
276
- * {@link LiveEntriesRead.totalLive}. Equal to `entries.length` for every
277
- * collection inside the bound, which is almost all of them.
278
- */
279
- totalLive: number;
280
327
  }
281
328
  /**
282
329
  * Published entries + category taxonomy for a collection resolved by slug —
@@ -307,10 +354,25 @@ export declare function getCollectionContent(options: {
307
354
  hostId: string;
308
355
  collectionSlug: string;
309
356
  entrySlug?: string;
310
- /** 1-based list page (AGL-620); with `perPage`, drives pagination metadata. */
357
+ /**
358
+ * 1-based list page (AGL-620), now a DISPLAY counter (AGL-3219): it labels
359
+ * the page the reader is on and no read is positioned from it. The entries
360
+ * come from `after`/`before`, or from the head of the cached source when
361
+ * neither is set.
362
+ */
311
363
  page?: number;
312
364
  /** Entries per page (AGL-620); when set the list is paginated. */
313
365
  perPage?: number;
366
+ /**
367
+ * Continue AFTER this entry's document id — the older direction (AGL-3219).
368
+ *
369
+ * What `/{collection}?after={id}` carries. A page defined this way does not
370
+ * move when something is published above it, which is the whole reason the
371
+ * addresses stopped being positions.
372
+ */
373
+ after?: string;
374
+ /** Continue BEFORE this entry's document id — the newer direction. */
375
+ before?: string;
314
376
  /**
315
377
  * Category segment of `/{collection}/category/{slug}` (AGL-1321). Filters
316
378
  * the listing before pagination is computed, so page counts and the page