@aglyn/aglyn 1.0.0-beta.218 → 1.0.0-beta.220

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.
Files changed (455) hide show
  1. package/package.json +12 -12
  2. package/src/lib/app-utils/actions.d.ts +26 -433
  3. package/src/lib/app-utils/actions.js +108 -633
  4. package/src/lib/app-utils/actions.js.map +1 -1
  5. package/src/lib/app-utils/activity-presenter.d.ts +3 -3
  6. package/src/lib/app-utils/activity-presenter.js +36 -52
  7. package/src/lib/app-utils/activity-presenter.js.map +1 -1
  8. package/src/lib/app-utils/analytics-events.d.ts +1 -1
  9. package/src/lib/app-utils/analytics-events.js.map +1 -1
  10. package/src/lib/app-utils/analytics-provider.d.ts +182 -0
  11. package/src/lib/app-utils/analytics-provider.js +133 -0
  12. package/src/lib/app-utils/analytics-provider.js.map +1 -0
  13. package/src/lib/app-utils/api-plugins.d.ts +46 -0
  14. package/src/lib/app-utils/api-plugins.js +19 -0
  15. package/src/lib/app-utils/api-plugins.js.map +1 -1
  16. package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
  17. package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
  18. package/src/lib/app-utils/artifact-overrides.js.map +1 -0
  19. package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
  20. package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
  21. package/src/lib/app-utils/artifact-provenance.js.map +1 -0
  22. package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
  23. package/src/lib/app-utils/binding-token-catalog.js +17 -41
  24. package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
  25. package/src/lib/app-utils/brand-support-line.d.ts +36 -0
  26. package/src/lib/app-utils/brand-support-line.js +38 -0
  27. package/src/lib/app-utils/brand-support-line.js.map +1 -0
  28. package/src/lib/app-utils/collection-entries.d.ts +2 -2
  29. package/src/lib/app-utils/collection-entries.js.map +1 -1
  30. package/src/lib/app-utils/consent-group-change.d.ts +4 -0
  31. package/src/lib/app-utils/consent-group-change.js.map +1 -1
  32. package/src/lib/app-utils/consent-groups.d.ts +1 -1
  33. package/src/lib/app-utils/consent-groups.js.map +1 -1
  34. package/src/lib/app-utils/console-routes.d.ts +15 -42
  35. package/src/lib/app-utils/console-routes.js +10 -39
  36. package/src/lib/app-utils/console-routes.js.map +1 -1
  37. package/src/lib/app-utils/contacts.d.ts +24 -2
  38. package/src/lib/app-utils/contacts.js +21 -5
  39. package/src/lib/app-utils/contacts.js.map +1 -1
  40. package/src/lib/app-utils/container-membership.d.ts +93 -0
  41. package/src/lib/app-utils/container-membership.js +165 -0
  42. package/src/lib/app-utils/container-membership.js.map +1 -0
  43. package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
  44. package/src/lib/app-utils/crm-task-reminders.js +32 -11
  45. package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
  46. package/src/lib/app-utils/crm.d.ts +24 -4
  47. package/src/lib/app-utils/crm.js +23 -0
  48. package/src/lib/app-utils/crm.js.map +1 -1
  49. package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
  50. package/src/lib/app-utils/cross-pool-desync.js +47 -0
  51. package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
  52. package/src/lib/app-utils/csv-upload.d.ts +29 -0
  53. package/src/lib/app-utils/csv-upload.js +29 -0
  54. package/src/lib/app-utils/csv-upload.js.map +1 -0
  55. package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
  56. package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
  57. package/src/lib/app-utils/csv.js.map +1 -0
  58. package/src/lib/app-utils/dataset-models.d.ts +5 -5
  59. package/src/lib/app-utils/dataset-models.js +19 -2
  60. package/src/lib/app-utils/dataset-models.js.map +1 -1
  61. package/src/lib/app-utils/datasets.d.ts +65 -30
  62. package/src/lib/app-utils/datasets.js +74 -24
  63. package/src/lib/app-utils/datasets.js.map +1 -1
  64. package/src/lib/app-utils/docs-deflection.d.ts +6 -6
  65. package/src/lib/app-utils/docs-deflection.js.map +1 -1
  66. package/src/lib/app-utils/docs-help.generated.d.ts +14 -8
  67. package/src/lib/app-utils/docs-help.generated.js +23 -6
  68. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  69. package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
  70. package/src/lib/app-utils/docs-index.generated.js +570 -346
  71. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  72. package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
  73. package/src/lib/app-utils/docs-retrieval.js +14 -10
  74. package/src/lib/app-utils/docs-retrieval.js.map +1 -1
  75. package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
  76. package/src/lib/app-utils/duplicate-resource.js +7 -4
  77. package/src/lib/app-utils/duplicate-resource.js.map +1 -1
  78. package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
  79. package/src/lib/app-utils/dynamic-list-rule.js +3 -3
  80. package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
  81. package/src/lib/app-utils/element-ui.d.ts +1 -1
  82. package/src/lib/app-utils/element-ui.js +1 -1
  83. package/src/lib/app-utils/element-ui.js.map +1 -1
  84. package/src/lib/app-utils/email-starter-policy.js +3 -3
  85. package/src/lib/app-utils/email-starter-policy.js.map +1 -1
  86. package/src/lib/app-utils/email-text.d.ts +80 -0
  87. package/src/lib/app-utils/email-text.js +189 -0
  88. package/src/lib/app-utils/email-text.js.map +1 -0
  89. package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
  90. package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
  91. package/src/lib/app-utils/enrollment-basis.js.map +1 -0
  92. package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
  93. package/src/lib/app-utils/entity-picker-context.js.map +1 -1
  94. package/src/lib/app-utils/error-beacon.d.ts +75 -6
  95. package/src/lib/app-utils/error-beacon.js +160 -21
  96. package/src/lib/app-utils/error-beacon.js.map +1 -1
  97. package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
  98. package/src/lib/app-utils/expand-repeatables.js +14 -14
  99. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  100. package/src/lib/app-utils/forms.js +2 -2
  101. package/src/lib/app-utils/forms.js.map +1 -1
  102. package/src/lib/app-utils/health-report.d.ts +2 -3
  103. package/src/lib/app-utils/health-report.js +8 -6
  104. package/src/lib/app-utils/health-report.js.map +1 -1
  105. package/src/lib/app-utils/host-events.d.ts +82 -0
  106. package/src/lib/app-utils/host-events.js +62 -0
  107. package/src/lib/app-utils/host-events.js.map +1 -0
  108. package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
  109. package/src/lib/app-utils/inbound-mail-domain.js +31 -0
  110. package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
  111. package/src/lib/app-utils/lockdown.d.ts +11 -0
  112. package/src/lib/app-utils/lockdown.js +33 -3
  113. package/src/lib/app-utils/lockdown.js.map +1 -1
  114. package/src/lib/app-utils/margin-utilization.d.ts +4 -69
  115. package/src/lib/app-utils/margin-utilization.js +82 -69
  116. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  117. package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
  118. package/src/lib/app-utils/member-email-aliases.js +29 -0
  119. package/src/lib/app-utils/member-email-aliases.js.map +1 -1
  120. package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
  121. package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
  122. package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
  123. package/src/lib/app-utils/node-interactions.d.ts +7 -7
  124. package/src/lib/app-utils/node-interactions.js.map +1 -1
  125. package/src/lib/app-utils/notifications.d.ts +147 -47
  126. package/src/lib/app-utils/notifications.js +182 -44
  127. package/src/lib/app-utils/notifications.js.map +1 -1
  128. package/src/lib/app-utils/operator-alerts.d.ts +15 -1
  129. package/src/lib/app-utils/operator-alerts.js +47 -10
  130. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  131. package/src/lib/app-utils/payments-configured.d.ts +8 -21
  132. package/src/lib/app-utils/payments-configured.js +8 -23
  133. package/src/lib/app-utils/payments-configured.js.map +1 -1
  134. package/src/lib/app-utils/plan-entitlements.d.ts +207 -306
  135. package/src/lib/app-utils/plan-entitlements.js +345 -528
  136. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  137. package/src/lib/app-utils/platform-brand.d.ts +10 -0
  138. package/src/lib/app-utils/platform-brand.js +14 -3
  139. package/src/lib/app-utils/platform-brand.js.map +1 -1
  140. package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
  141. package/src/lib/app-utils/platform-marketing-consent.js +6 -5
  142. package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
  143. package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
  144. package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
  145. package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
  146. package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
  147. package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
  148. package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
  149. package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
  150. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
  151. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
  152. package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
  153. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
  154. package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
  155. package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
  156. package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
  157. package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
  158. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  159. package/src/lib/app-utils/release-flags.d.ts +14 -5
  160. package/src/lib/app-utils/release-flags.js +21 -125
  161. package/src/lib/app-utils/release-flags.js.map +1 -1
  162. package/src/lib/app-utils/repeat-query.d.ts +56 -0
  163. package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
  164. package/src/lib/app-utils/repeat-query.js.map +1 -0
  165. package/src/lib/app-utils/repeat-sources.d.ts +9 -0
  166. package/src/lib/app-utils/repeat-sources.js.map +1 -1
  167. package/src/lib/app-utils/retained-refusals.d.ts +73 -0
  168. package/src/lib/app-utils/retained-refusals.js +186 -0
  169. package/src/lib/app-utils/retained-refusals.js.map +1 -0
  170. package/src/lib/app-utils/scope-tokens.d.ts +27 -0
  171. package/src/lib/app-utils/scope-tokens.js +61 -0
  172. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  173. package/src/lib/app-utils/screen-link-context.js +3 -3
  174. package/src/lib/app-utils/screen-link-context.js.map +1 -1
  175. package/src/lib/app-utils/screen-route.js +1 -1
  176. package/src/lib/app-utils/screen-route.js.map +1 -1
  177. package/src/lib/app-utils/seo-audit.d.ts +132 -0
  178. package/src/lib/app-utils/seo-audit.js +246 -0
  179. package/src/lib/app-utils/seo-audit.js.map +1 -0
  180. package/src/lib/app-utils/seo-keywords.d.ts +48 -0
  181. package/src/lib/app-utils/seo-keywords.js +60 -0
  182. package/src/lib/app-utils/seo-keywords.js.map +1 -0
  183. package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
  184. package/src/lib/app-utils/seo-page-facts.js +155 -0
  185. package/src/lib/app-utils/seo-page-facts.js.map +1 -0
  186. package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
  187. package/src/lib/app-utils/seo-site-scan.js +162 -0
  188. package/src/lib/app-utils/seo-site-scan.js.map +1 -0
  189. package/src/lib/app-utils/seo-title-variables.js +1 -1
  190. package/src/lib/app-utils/seo-title-variables.js.map +1 -1
  191. package/src/lib/app-utils/server.d.ts +13 -18
  192. package/src/lib/app-utils/server.js +33 -51
  193. package/src/lib/app-utils/server.js.map +1 -1
  194. package/src/lib/app-utils/site-interactions.d.ts +501 -0
  195. package/src/lib/app-utils/site-interactions.js +554 -0
  196. package/src/lib/app-utils/site-interactions.js.map +1 -0
  197. package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
  198. package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +2 -2
  199. package/src/lib/app-utils/site-theme.js.map +1 -0
  200. package/src/lib/app-utils/sitemap.d.ts +4 -3
  201. package/src/lib/app-utils/sitemap.js +9 -7
  202. package/src/lib/app-utils/sitemap.js.map +1 -1
  203. package/src/lib/app-utils/stale-build-error.d.ts +51 -0
  204. package/src/lib/app-utils/stale-build-error.js +100 -1
  205. package/src/lib/app-utils/stale-build-error.js.map +1 -1
  206. package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
  207. package/src/lib/app-utils/starter-template-nodes.js +129 -0
  208. package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
  209. package/src/lib/app-utils/starter-templates.d.ts +11 -34
  210. package/src/lib/app-utils/starter-templates.js +19 -230
  211. package/src/lib/app-utils/starter-templates.js.map +1 -1
  212. package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
  213. package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
  214. package/src/lib/app-utils/subscription-topics.js.map +1 -0
  215. package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
  216. package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
  217. package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
  218. package/src/lib/app-utils/theme-library.d.ts +10 -3
  219. package/src/lib/app-utils/theme-library.js +13 -4
  220. package/src/lib/app-utils/theme-library.js.map +1 -1
  221. package/src/lib/app-utils/usage-budget.d.ts +73 -188
  222. package/src/lib/app-utils/usage-budget.js +58 -194
  223. package/src/lib/app-utils/usage-budget.js.map +1 -1
  224. package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
  225. package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
  226. package/src/lib/app-utils/utm-attribution.js.map +1 -0
  227. package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
  228. package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
  229. package/src/lib/app-utils/utm-forwarding.js.map +1 -0
  230. package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +25 -17
  231. package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +38 -30
  232. package/src/lib/app-utils/utm-touch.js.map +1 -0
  233. package/src/lib/app-utils/variables.d.ts +9 -9
  234. package/src/lib/app-utils/variables.js.map +1 -1
  235. package/src/lib/app-utils/video-object.js +6 -5
  236. package/src/lib/app-utils/video-object.js.map +1 -1
  237. package/src/lib/app-utils/visitor-consent.d.ts +41 -164
  238. package/src/lib/app-utils/visitor-consent.js +66 -277
  239. package/src/lib/app-utils/visitor-consent.js.map +1 -1
  240. package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
  241. package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
  242. package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
  243. package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
  244. package/src/lib/app-utils/web-vitals-rum.js +31 -63
  245. package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
  246. package/src/lib/app-utils/where-used.d.ts +11 -6
  247. package/src/lib/app-utils/where-used.js +3 -8
  248. package/src/lib/app-utils/where-used.js.map +1 -1
  249. package/src/lib/foundation/definitions/components.types.d.ts +1 -1
  250. package/src/lib/foundation/definitions/components.types.js +1 -1
  251. package/src/lib/foundation/definitions/components.types.js.map +1 -1
  252. package/src/lib/foundation/definitions/org-billing.types.d.ts +11 -137
  253. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  254. package/src/lib/foundation/definitions/organization.types.d.ts +1 -3
  255. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  256. package/src/lib/foundation/definitions/platform.types.d.ts +11 -5
  257. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  258. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
  259. package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
  260. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  261. package/src/lib/foundation/index.d.ts +1 -1
  262. package/src/lib/foundation/index.js.map +1 -1
  263. package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
  264. package/src/lib/plugin-manager/besigner-documents.js +61 -0
  265. package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
  266. package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
  267. package/src/lib/plugin-manager/computed-variables.js +74 -0
  268. package/src/lib/plugin-manager/computed-variables.js.map +1 -0
  269. package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
  270. package/src/lib/plugin-manager/custom-fields.js +47 -1
  271. package/src/lib/plugin-manager/custom-fields.js.map +1 -1
  272. package/src/lib/plugin-manager/feature-plugins.d.ts +372 -99
  273. package/src/lib/plugin-manager/feature-plugins.js +126 -51
  274. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  275. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +134 -0
  276. package/src/lib/plugin-manager/first-party-plugins.generated.js +840 -10
  277. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  278. package/src/lib/plugin-manager/index.d.ts +0 -1
  279. package/src/lib/plugin-manager/index.js +0 -1
  280. package/src/lib/plugin-manager/index.js.map +1 -1
  281. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
  282. package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
  283. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  284. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
  285. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  286. package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
  287. package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
  288. package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
  289. package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
  290. package/src/lib/plugin-manager/plugin-containers.js +27 -0
  291. package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
  292. package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
  293. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  294. package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
  295. package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
  296. package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
  297. package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
  298. package/src/lib/plugin-manager/plugin-dependents.js +75 -0
  299. package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
  300. package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
  301. package/src/lib/plugin-manager/plugin-distribution.js +45 -0
  302. package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
  303. package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
  304. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  305. package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
  306. package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
  307. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
  308. package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
  309. package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
  310. package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
  311. package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
  312. package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
  313. package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
  314. package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
  315. package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
  316. package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
  317. package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
  318. package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
  319. package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
  320. package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
  321. package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
  322. package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
  323. package/src/lib/plugin-manager/plugin-record-index.js +51 -0
  324. package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
  325. package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
  326. package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
  327. package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
  328. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
  329. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  330. package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
  331. package/src/lib/plugin-manager/plugin-record-written.js +42 -0
  332. package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
  333. package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
  334. package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
  335. package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
  336. package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
  337. package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
  338. package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
  339. package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
  340. package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
  341. package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
  342. package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
  343. package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
  344. package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
  345. package/src/lib/plugin-manager/plugin-site-export.js +40 -0
  346. package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
  347. package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
  348. package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
  349. package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
  350. package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
  351. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
  352. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
  353. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
  354. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  355. package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
  356. package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
  357. package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
  358. package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
  359. package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
  360. package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
  361. package/src/lib/plugin-manager/plugin-usage-axes.d.ts +319 -0
  362. package/src/lib/plugin-manager/plugin-usage-axes.js +186 -0
  363. package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
  364. package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
  365. package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
  366. package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
  367. package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
  368. package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
  369. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
  370. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
  371. package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
  372. package/src/lib/plugin-manager/record-captured-contact.js +10 -32
  373. package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
  374. package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
  375. package/src/lib/plugin-manager/repeat-rows.js +108 -0
  376. package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
  377. package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
  378. package/src/lib/plugin-manager/submission-record-target.js +125 -0
  379. package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
  380. package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
  381. package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
  382. package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
  383. package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
  384. package/src/lib/plugin-manager/video-embed-provider.js +165 -0
  385. package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
  386. package/src/lib/types/nodes.d.ts +2 -2
  387. package/src/lib/types/nodes.js.map +1 -1
  388. package/src/lib/app-utils/assist-credits.d.ts +0 -482
  389. package/src/lib/app-utils/assist-credits.js +0 -509
  390. package/src/lib/app-utils/assist-credits.js.map +0 -1
  391. package/src/lib/app-utils/campaign-attribution.js.map +0 -1
  392. package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
  393. package/src/lib/app-utils/campaign-membership.d.ts +0 -121
  394. package/src/lib/app-utils/campaign-membership.js +0 -189
  395. package/src/lib/app-utils/campaign-membership.js.map +0 -1
  396. package/src/lib/app-utils/campaign-touch.js.map +0 -1
  397. package/src/lib/app-utils/console-record-links.d.ts +0 -163
  398. package/src/lib/app-utils/console-record-links.js +0 -180
  399. package/src/lib/app-utils/console-record-links.js.map +0 -1
  400. package/src/lib/app-utils/contact-holder.d.ts +0 -68
  401. package/src/lib/app-utils/contact-holder.js +0 -129
  402. package/src/lib/app-utils/contact-holder.js.map +0 -1
  403. package/src/lib/app-utils/contact-merge.d.ts +0 -136
  404. package/src/lib/app-utils/contact-merge.js +0 -412
  405. package/src/lib/app-utils/contact-merge.js.map +0 -1
  406. package/src/lib/app-utils/crm-booking.d.ts +0 -75
  407. package/src/lib/app-utils/crm-booking.js +0 -133
  408. package/src/lib/app-utils/crm-booking.js.map +0 -1
  409. package/src/lib/app-utils/crm-csv.d.ts +0 -239
  410. package/src/lib/app-utils/crm-csv.js +0 -467
  411. package/src/lib/app-utils/crm-csv.js.map +0 -1
  412. package/src/lib/app-utils/crm-digest.d.ts +0 -185
  413. package/src/lib/app-utils/crm-digest.js +0 -272
  414. package/src/lib/app-utils/crm-digest.js.map +0 -1
  415. package/src/lib/app-utils/crm-inbound.d.ts +0 -245
  416. package/src/lib/app-utils/crm-inbound.js +0 -407
  417. package/src/lib/app-utils/crm-inbound.js.map +0 -1
  418. package/src/lib/app-utils/crm-reports.d.ts +0 -479
  419. package/src/lib/app-utils/crm-reports.js +0 -680
  420. package/src/lib/app-utils/crm-reports.js.map +0 -1
  421. package/src/lib/app-utils/dataset-csv.js.map +0 -1
  422. package/src/lib/app-utils/dataset-query.d.ts +0 -57
  423. package/src/lib/app-utils/dataset-query.js.map +0 -1
  424. package/src/lib/app-utils/email-topics.js.map +0 -1
  425. package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
  426. package/src/lib/app-utils/form-dataset-binding.js +0 -74
  427. package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
  428. package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
  429. package/src/lib/app-utils/list-import.d.ts +0 -158
  430. package/src/lib/app-utils/list-import.js +0 -332
  431. package/src/lib/app-utils/list-import.js.map +0 -1
  432. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
  433. package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
  434. package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
  435. package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
  436. package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
  437. package/src/lib/app-utils/marketplace-theme.js.map +0 -1
  438. package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
  439. package/src/lib/app-utils/marketplace-update-state.js +0 -132
  440. package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
  441. package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
  442. package/src/lib/app-utils/publisher-agreement.js +0 -321
  443. package/src/lib/app-utils/publisher-agreement.js.map +0 -1
  444. package/src/lib/app-utils/wistia-embed.d.ts +0 -104
  445. package/src/lib/app-utils/wistia-embed.js +0 -138
  446. package/src/lib/app-utils/wistia-embed.js.map +0 -1
  447. package/src/lib/app-utils/workflows.d.ts +0 -136
  448. package/src/lib/app-utils/workflows.js +0 -346
  449. package/src/lib/app-utils/workflows.js.map +0 -1
  450. package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
  451. package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
  452. package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
  453. package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
  454. package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
  455. package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/collection-entries.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 type { AglynNodeSchema, NodeId } from '../foundation'\nimport {\n COLLECTION_ENTRY_DATE_FORMAT_DEFAULT,\n type CollectionEntryDateFormat,\n collectionEntryPublishedAtIso,\n formatCollectionEntryDate,\n normalizeCollectionEntryDateFormat,\n} from './collection-entry-date'\nimport {\n type ContentAuthorLink,\n type ContentAuthorRecord,\n contentAuthorPageUrl,\n normalizeContentAuthorLinks,\n} from './content-authors'\nimport { resolveNamedTokens } from './resolve-named-tokens'\n\nexport * from './collection-entry-date'\n\n/**\n * Persisted component id of the \"Collection entries\" repeater block\n * (plugins-mui). Like `layoutSlot`, the id lives here so the tenant compose\n * pipeline can find the block without importing the component bundle.\n */\nexport const COLLECTION_ENTRIES_COMPONENT_ID = 'collectionEntries'\n\n/** Persisted component id of the markdown \"Entry body\" block (plugins-mui). */\nexport const COLLECTION_ENTRY_BODY_COMPONENT_ID = 'collectionEntryBody'\n\n/**\n * Persisted component id of the \"Related posts\" block (plugins-mui,\n * AGL-582). The compose pipeline stamps the related entries onto the node,\n * so the id lives here like the entries/body blocks.\n */\nexport const COLLECTION_RELATED_COMPONENT_ID = 'collectionRelated'\n\n/** Persisted component id of the share-bar block (plugins-mui, AGL-582). */\nexport const COLLECTION_SHARE_COMPONENT_ID = 'collectionShare'\n\n/** Persisted component id of the entry-meta block (plugins-mui, AGL-582). */\nexport const COLLECTION_ENTRY_META_COMPONENT_ID = 'collectionEntryMeta'\n\n/**\n * Persisted component id of the author card that closes an article\n * (plugins-mui, AGL-2486).\n *\n * Entry Meta prints the byline — a NAME, on one line, beside the date. The\n * author record it now resolves against carries a portrait, a bio, a job\n * title and a url, and none of those have anywhere to render: a template that\n * wanted the card the article frame draws had to type the name and the blurb\n * as literal text, which then said \"The Aglyn Team\" under a post somebody\n * else wrote and went stale the moment the author record was edited. The id\n * lives here, with the other collection blocks, so the compose pipeline can\n * fill the card without importing the bundle.\n */\nexport const COLLECTION_ENTRY_AUTHOR_COMPONENT_ID = 'collectionEntryAuthor'\n\n/**\n * Persisted component id of the \"Category Pills\" block (plugins-mui,\n * AGL-1321). Like the entries repeater, the id lives here so the tenant\n * compose pipeline can stamp the block's links without importing the bundle.\n */\nexport const COLLECTION_CATEGORIES_COMPONENT_ID = 'collectionCategories'\n\n/**\n * Persisted component id of the standalone \"Collection Search\" block\n * (plugins-mui, AGL-1516).\n *\n * The Collection Entries block grew its own search box first, and it renders\n * INSIDE that block — first child of the block's own stack. Figma 494:1220\n * puts the field in the listing's toolbar row instead, beside the RSS button\n * with the category pills opposite it, and no amount of authoring can lift a\n * block's own child out of it. The pills cannot move the other way either:\n * every child of an entries block is cloned once per entry.\n *\n * So the field became a block an author can put where the frame draws it.\n * Suggestion-panel only, by construction rather than by omission — a\n * standalone box has no clones to hide, so `filter` is a thing it genuinely\n * cannot do, and the one behaviour it does have is the toolbar behaviour the\n * frame asks for.\n */\nexport const COLLECTION_SEARCH_COMPONENT_ID = 'collectionSearch'\n\n/**\n * Reserved list sub-paths (AGL-620 `page`, AGL-1321 `category`). Both are\n * only reserved as the HEAD of a longer path, so an entry legitimately\n * slugged `page` or `category` keeps serving at `/{collection}/{entry}`.\n */\nexport const COLLECTION_PAGE_ROUTE_SEGMENT = 'page'\nexport const COLLECTION_CATEGORY_ROUTE_SEGMENT = 'category'\n/**\n * `/{collection}/after/{cursor}` — the older page (AGL-3219).\n *\n * ## Why a PATH segment and not `?after=`\n *\n * The tenant catch-all is statically rendered and ISR-cached with the URL\n * PATH as its key (`revalidate = 3600`, AGL-1152). A query string is not part\n * of that key, so every cursor address would share ONE cache entry and serve\n * whichever of them rendered first to everybody — the same reason a routed\n * category is `/{collection}/category/{slug}` rather than `?category=`. And\n * reading `searchParams` at all opts the whole catch-all out of static\n * rendering, which is the opposite of what that page is for.\n *\n * A path segment is cached per address, crawlable, and costs nothing.\n */\nexport const COLLECTION_AFTER_ROUTE_SEGMENT = 'after'\n/** `/{collection}/before/{cursor}` — the newer page (AGL-3219). */\nexport const COLLECTION_BEFORE_ROUTE_SEGMENT = 'before'\n\n/** Namespaces cloned template ids per container/entry (cf. `rep__`). */\nexport const COLLECTION_ENTRIES_NODE_ID_PREFIX = 'centry__'\n\n/** Hard bound on entries a single block renders, before `entriesLimit`. */\nexport const COLLECTION_ENTRIES_MAX = 100\n\n/**\n * Hard bound on the entries one READ of a collection returns (AGL-1516) — the\n * `.limit()` the tenant's live-entries query carries.\n *\n * Named here, beside the render bound it has always silently matched, because\n * a search index has to know the difference. The render bound is a choice\n * about a block; this one is a choice about the DATA, and a set that reaches\n * it is a set nobody has seen all of. Two 100s that happened to agree meant\n * `entries.length === 100` could be read either as \"this collection has 100\n * entries\" or \"this read stopped at 100\", and the honest empty state turns on\n * exactly that distinction.\n */\nexport const COLLECTION_SOURCE_MAX = 100\n\n/**\n * Whether a collection source is a COMPLETE read or a bounded one\n * (AGL-1516) — true when the read came back holding its own limit, which is\n * the only signal a limited query without a count leaves behind.\n *\n * Deliberately errs toward \"bounded\": a collection of exactly\n * {@link COLLECTION_SOURCE_MAX} entries answers true and gets described one\n * notch more cautiously than it needed. Overstating what a search covered is\n * the failure that renders as a confident \"nothing matched\".\n */\nexport function collectionSourceReachedBound(\n entries: readonly unknown[] | undefined,\n): boolean {\n return (entries?.length ?? 0) >= COLLECTION_SOURCE_MAX\n}\n\n/**\n * Whether the read behind a collection source was a bounded one (AGL-1516),\n * asking the READER first and only then falling back to counting.\n *\n * The count is a proxy, and it under-reports in two ways that both landed\n * after `collectionSourceReachedBound` was written:\n *\n * 1. The query takes `status in ['published', 'scheduled']` and the loader\n * then drops what is not live yet — a future `publishAt`, or since AGL-471\n * a due schedule the plan does not carry. A read that came back holding\n * its full 100 docs can therefore hand over 96 entries, and 96 does not\n * look like a ceiling. A blog with a scheduling queue is exactly the site\n * that hits this.\n * 2. A category route filters `entries` before compose ever sees them, so the\n * \"raw set\" the entries block is careful to measure is already narrowed.\n *\n * Both make the flag read FALSE on a read that really did stop at its bound,\n * and false is the direction that produces the confident \"nothing matched\"\n * this signal exists to prevent. So `reachedBound` is threaded down from the\n * query that owns the fact, and the count survives only as the fallback for a\n * source that predates it — a payload cached before this shipped, or a caller\n * that assembles a source by hand.\n */\nexport function collectionSourceIsBounded(\n source: Pick<CollectionEntriesSource, 'entries' | 'reachedBound'> | undefined,\n): boolean {\n if (source?.reachedBound) return true\n return collectionSourceReachedBound(source?.entries)\n}\n\n/** Default entries per page for a paginated collection list (AGL-620). */\nexport const COLLECTION_LIST_PAGE_SIZE = 10\n\n/**\n * Total pages for `total` entries at `perPage` per page — at least 1 so an\n * empty collection still has a page 1 (AGL-620).\n */\nexport function collectionTotalPages(total: number, perPage: number): number {\n if (perPage <= 0) return 1\n return Math.max(1, Math.ceil(total / perPage))\n}\n\n/**\n * The slice of a listing's entries that the page named by `pagination`\n * actually shows (AGL-3213).\n *\n * The loader hands the WHOLE narrowed set down to compose, because\n * {@link expandCollectionEntries} does its own windowing off the block's\n * props — a Collection entries block may carry its own `perPage`, its own\n * pinned `page`, a `firstPageOnly` switch and its own filters, none of which\n * this function can see. Slicing before compose would double-window and empty\n * every page after the first.\n *\n * Everything the loader serializes into page PROPS is downstream of compose,\n * and there the full set is not merely wasted bytes — it is a false\n * statement. The `ItemList` JSON-LD numbers its items\n * `(page - 1) * perPage + index + 1` on the premise that `entries` is the\n * window; handed all one hundred, `/changelog/page/7` published\n * `numberOfItems: 100` and positions 61 through 160, most of them past the\n * end of the collection. The legacy fallback renderer read it the same way\n * and rendered the whole list on every page.\n *\n * So: the loader keeps the full set until compose has had it, and narrows\n * HERE on the way into props, where `pagination` is the route's own answer to\n * which page this is.\n *\n * An unpaginated listing (an entry route, the RSS feed) passes no pagination\n * and gets its entries back untouched.\n */\nexport function collectionEntriesPageWindow<T>(\n entries: readonly T[],\n pagination:\n | { page?: number; perPage?: number; windowStart?: number }\n | null\n | undefined,\n): T[] {\n const perPage = Number(pagination?.perPage)\n if (!Number.isFinite(perPage) || perPage <= 0) return [...entries]\n const pageRaw = Number(pagination?.page)\n const page = Number.isFinite(pageRaw) && pageRaw >= 1 ? Math.floor(pageRaw) : 1\n // `windowStart` is where `entries` begins in the collection's own order\n // (AGL-3213). A listing past the cached read's bound is served by a window\n // read for that page alone, and slicing it from the page's absolute offset\n // would return nothing.\n const startRaw = Number(pagination?.windowStart)\n const windowStart =\n Number.isFinite(startRaw) && startRaw > 0 ? Math.floor(startRaw) : 0\n const start = Math.max(0, (page - 1) * Math.floor(perPage) - windowStart)\n return entries.slice(start, start + Math.floor(perPage))\n}\n\n/** Default/most related posts a Related posts block renders (AGL-582). */\nexport const COLLECTION_RELATED_DEFAULT_LIMIT = 3\nexport const COLLECTION_RELATED_MAX = 12\n\n/** Most categories a collection's taxonomy holds (AGL-582). */\nexport const COLLECTION_CATEGORIES_MAX = 50\n\n/**\n * One taxonomy category on a COLLECTION doc (AGL-582): `id` is the stable\n * slug-like key entries reference (`categoryId`), generated once from the\n * initial name and never changed; `name` is the display label and freely\n * renameable. Stored as an array to preserve author-defined ordering.\n */\nexport interface CollectionCategory {\n id: string\n name: string\n /**\n * What this category's listing is about, for the `<meta name=\"description\">`\n * of `/{collection}/category/{slug}`.\n *\n * Without one every filtered listing on a site inherits the list template\n * screen's description — six `/blog/category/*` URLs all described as \"the\n * blog\", which is the same duplicate-result problem the composed category\n * TITLE was introduced to solve (AGL-1321), one tag over. Optional, and the\n * template's description remains the fallback: a category nobody has\n * described is better summarized by the list it filters than by nothing.\n */\n description?: string\n}\n\n/**\n * How many LIVE entries one content collection may hold (AGL-2266).\n *\n * `hosts/{hostId}/collections/{cid}/entries/{eid}` had a DEDICATED rules block\n * re-granting `create` to any editor, client-direct — deliberately, because the\n * name-based exclusions on the host catch-all must not reach entries — so it\n * was the one quota-governed shape under a host with no quota at all. A free\n * org could mint unbounded Firestore documents from the browser.\n *\n * This is a flat PLATFORM cap, not a plan dimension. AGL-1387 declined\n * `collectionsPerHost` and this does not re-open it: nothing here is priced,\n * no `OrgEntitlements` key is added, and every plan gets the same number. It is\n * the `WEBHOOK_MAX_PER_HOST` (AGL-1360) / `NON_PAGE_SCREEN_MAX_PER_HOST`\n * (AGL-1399) shape, which both issues invented for exactly this: uncapped\n * infrastructure behind a $0 subscription.\n *\n * ## Per COLLECTION, paired with {@link COLLECTIONS_MAX_PER_HOST}\n *\n * A per-HOST entry cap would need a count across every collection the host\n * holds, which is a collection-group read the entry documents carry no `hostId`\n * to scope. Per collection is one `count()` on the collection the create is\n * addressed to — the same read every other cap on `/api/hosts/resources`\n * already pays — and the host is bounded by the PRODUCT of the two numbers,\n * which is why the sibling cap had to land in the same change. One of the two\n * alone bounds nothing: unbounded collections each holding 10,000 entries is\n * the same unbounded store, spelled differently.\n *\n * ## Why 10,000\n *\n * A blog publishing daily reaches it after twenty-seven years; the largest\n * content collection in production holds double digits. The failure mode of a\n * flat cap set too low is blocking real authoring with an error the price list\n * cannot explain, so it is sized to the heaviest plausible library rather than\n * to today's data. It also bounds the cost of the console's own listing, which\n * reads entries with `limit(200)` and would otherwise page through a store with\n * no ceiling.\n */\nexport const ENTRIES_MAX_PER_COLLECTION = 10000\n\n/**\n * How many LIVE content collections one host may hold (AGL-2266).\n *\n * The other half of the entry bound above, and the reason it means anything.\n * Collection creation was already server-owned — the rules deny client `create`\n * and `/api/hosts/collections` claims the slug transactionally (AGL-978) — so\n * the collection had a WRITER with authority and simply no number to enforce.\n *\n * Again a platform cap and not `collectionsPerHost`: AGL-1387 decided\n * collections are not a thing customers are charged for, and that decision is\n * untouched. 100 is far past any real site's taxonomy — production's busiest\n * host has a handful — and short enough that the entries ceiling above\n * multiplies out to a bounded store rather than an unbounded one.\n */\nexport const COLLECTIONS_MAX_PER_HOST = 100\n\n/**\n * One published content-collection entry as the compose pipeline sees it\n * (AGL-551). Mirrors the tenant's `CollectionEntrySummary` without the\n * Firestore dependency so the expansion stays pure.\n */\nexport interface CollectionEntryRecord {\n $id?: string\n title?: string\n slug?: string\n excerpt?: string\n /**\n * Per-entry byline (AGL-686); falls back to the site as author.\n *\n * Since AGL-2486 this is either the legacy free-typed string or the NAME of\n * the author record `authorId` points at, denormalized by the tenant runtime\n * so the byline block, the `{{entry.author}}` token and the RSS feed all\n * keep reading one field.\n */\n authorName?: string\n /**\n * Reference into `hosts/{hostId}/authors` (AGL-2486) — the custom author\n * this entry publishes under. Absent on every entry written before it, and\n * on any entry whose editor chose a one-off byline instead of a record.\n */\n authorId?: string\n /**\n * The resolved author record, attached by the tenant runtime alongside the\n * `authorName` it denormalizes (`get-collection-content.ts`). Carries the\n * portrait, which is why the byline can show the author's own face rather\n * than the site's mark — see {@link expandCollectionEntryMeta}.\n */\n author?: ContentAuthorRecord | null\n body?: string\n coverImage?: string\n /**\n * The entry's featured video (AGL-2956), stored in the shape\n * {@link coverImage} is: a `media:{scope}/{id}` reference or a URL, a Wistia\n * media link included. Raw here for the reason every image field in this\n * file is raw — the resolver needs the rendering host, which this record\n * does not carry.\n *\n * An entry of a video collection is an article whose featured video sits\n * where a post's cover image would, which is what makes its page a watch\n * page. `{{entry.coverVideo}}` binds it into a template's Video element, and\n * the built-in entry page plays it with no template at all.\n */\n coverVideo?: string\n /** Search-result title override (AGL-582); falls back to `title`. */\n seoTitle?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /**\n * Stable reference into the collection's `categories` taxonomy\n * (AGL-582): the ID survives renames, so display names resolve at\n * render via {@link resolveEntryCategoryName}.\n */\n categoryId?: string\n /**\n * Legacy free-typed bucket (AGL-582), e.g. \"Engineering\". Still READ\n * as a fallback everywhere; the editor writes `categoryId` going\n * forward.\n */\n category?: string\n /** Free-form labels (AGL-582), e.g. [\"nextjs\", \"seo\"]. */\n tags?: string[]\n publishedAt?: { seconds: number } | null\n /**\n * The collection this entry came OUT of, stamped where the read happened\n * (AGL-2518).\n *\n * Absent everywhere the routed collection already answers the question,\n * which is every listing the platform had until now: a `/blog` page renders\n * blog entries, so `entry.url` is built from the one slug the route\n * resolved and no entry needs to carry its own.\n *\n * The author page breaks that assumption — it lists a blog post, a\n * changelog note and a press release side by side, and building all three\n * URLs from a single routed slug would produce two links to pages that do\n * not exist. So the reader that mixes collections stamps each entry with\n * where it came from, and {@link collectionEntryTokens} prefers it. Every\n * other caller keeps passing one slug and keeps behaving identically.\n */\n collectionSlug?: string\n /**\n * The display name of {@link collectionSlug} — \"Changelog\", not\n * \"changelog\" — so a card on a mixed listing can label which section a\n * post belongs to. Stamped by the same reader, for the same reason.\n */\n collectionName?: string\n}\n\n/** A collection's published entries, keyed for expansion by its slug. */\nexport interface CollectionEntriesSource {\n slug: string\n entries: CollectionEntryRecord[]\n /** The collection's category taxonomy (AGL-582), for name resolution. */\n categories?: CollectionCategory[]\n /**\n * Whether the READ that produced `entries` came back holding its own\n * `.limit()` (AGL-1516) — a fact recorded where the query ran, because by\n * the time `entries` gets here it has been through a liveness filter and\n * possibly a route's category filter, and its length no longer answers the\n * question. See {@link collectionSourceIsBounded}.\n */\n reachedBound?: boolean\n /**\n * The page the ROUTE asked for (AGL-1321), set only for the collection the\n * URL resolved. Fills in a block that declares `perPage` but no `page` —\n * which is every block a designer can author, since design time cannot\n * know which page a visitor is on. A block that names its own `page` still\n * wins: that is a deliberately pinned window, not a paginated list.\n */\n page?: number\n /**\n * The absolute index `entries[0]` holds in the collection's own order\n * (AGL-3213). Zero everywhere except a listing page past the cached read's\n * bound, which is served by a window read for that page alone.\n *\n * Without it a windowed source is silently empty: every consumer here\n * slices `[(page - 1) * perPage, …)` on the premise that `entries` starts\n * at the beginning of the collection, so page 11 of a windowed ten-entry\n * source slices from 100 and renders nothing.\n */\n windowStart?: number\n}\n\n/**\n * Display name of an entry's category (AGL-582): the `categoryId` lookup\n * into the collection's taxonomy wins; the legacy free-typed\n * `entry.category` string is the fallback so existing data keeps\n * rendering. An ID referencing a deleted category resolves to nothing —\n * by design the entry simply shows no category.\n */\nexport function resolveEntryCategoryName(\n entry: Pick<CollectionEntryRecord, 'categoryId' | 'category'>,\n // `readonly` because this only reads (AGL-1321): the route helpers hold\n // the taxonomy as readonly and would otherwise have to copy it to ask.\n categories?: readonly CollectionCategory[],\n): string | undefined {\n const categoryId = (entry.categoryId ?? '').trim()\n if (categoryId) {\n const match = (categories ?? []).find(\n (category) => category.id === categoryId,\n )\n if (match?.name) return match.name\n }\n const legacy = (entry.category ?? '').trim()\n return legacy || undefined\n}\n\n/* ── Published-date formats (AGL-1459) ──────────────────────────────────── */\n\n\n/**\n * The values an Entry Meta block shows, in the exact spellings the\n * `{{entry.date}}` / `{{entry.author}}` / `{{entry.category}}` /\n * `{{entry.tags}}` tokens produce (AGL-1385, author added AGL-1459).\n *\n * Extracted so the token map and {@link expandCollectionEntryMeta} cannot\n * drift: the same entry has to read the same way whether the author bound the\n * tokens by hand or let the server fill the block in.\n */\nexport function collectionEntryMetaValues(\n entry: CollectionEntryRecord,\n categories?: readonly CollectionCategory[],\n dateFormat?: CollectionEntryDateFormat,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): { date: string; author: string; category: string; tags: string } {\n return {\n date: formatCollectionEntryDate(\n entry.publishedAt,\n dateFormat,\n undefined,\n timeZone,\n ),\n // The per-entry byline the editor already collects (AGL-686). It was\n // reachable in the entry's JSON-LD and nowhere on the page, which is what\n // made the frame's byline unauthorable (AGL-1459).\n //\n // The resolved RECORD first, then the legacy string (AGL-2486) — the\n // precedence `resolveEntryAuthor` applies, spelled the same way here. The\n // tenant denormalizes the record's name onto `authorName` before this\n // runs, so on a live render the two agree; reading the record directly is\n // what makes that a convenience rather than the only thing holding the\n // byline up.\n author: (entry.author?.name || entry.authorName || '').trim(),\n // Entry model v2 (AGL-582): category resolves by stable ID against the\n // collection's taxonomy, falling back to the legacy free-typed string.\n category: resolveEntryCategoryName(entry, categories) ?? '',\n tags: (entry.tags ?? []).join(', '),\n }\n}\n\n/**\n * The values an Entry Author card shows (AGL-2486) — the fields of the\n * resolved author RECORD, in the spellings the `{{entry.author*}}` tokens\n * produce.\n *\n * Extracted for the reason {@link collectionEntryMetaValues} was: the card,\n * the token map and {@link expandCollectionEntryAuthor} have to agree, and\n * three copies of \"name, falling back to the legacy string\" would not.\n *\n * `image` stays RAW — a `media:` reference or a plain url, whichever the\n * author saved — because the resolver that turns one into a fetchable src\n * needs the rendering host and this function has no host (`resolveMediaSrc`,\n * AGL-1215). Every other consumer of an image field in this file does the\n * same.\n */\nexport function collectionEntryAuthorValues(entry: CollectionEntryRecord): {\n name: string\n bio: string\n image: string\n url: string\n pageUrl: string\n links: ContentAuthorLink[]\n} {\n const author = entry.author\n return {\n // Their page on THIS site (AGL-2518/2519) — a different destination from\n // `url`, which is their own. Empty when nothing addresses them, so the\n // byline renders as plain text rather than as a link to `/author/`.\n pageUrl: contentAuthorPageUrl({\n ...(author ? { author } : {}),\n ...(entry.authorId ? { authorId: entry.authorId } : {}),\n ...(entry.authorName ? { authorName: entry.authorName } : {}),\n }),\n // The rows the card PRINTS (AGL-2516). Normalized here rather than\n // trusted, because this is the boundary a stored document crosses to\n // become props: the renderer guards the href too, but only one of the two\n // sits on the path a hand-written document would take.\n links: normalizeContentAuthorLinks(author?.links),\n // The record wins, then the legacy free-typed byline (AGL-686), which is\n // the same precedence `resolveEntryAuthor` applies — an entry written\n // before custom authors still fills a card, with the one field it has.\n name: (author?.name ?? entry.authorName ?? '').trim(),\n bio: (author?.bio ?? '').trim(),\n image: (author?.image ?? '').trim(),\n url: (author?.url ?? '').trim(),\n }\n}\n\n/**\n * The `{{entry.*}}` token map for one entry (AGL-105/551): substituted\n * globally on entry-template screens and per-clone inside the Collection\n * entries block. `entry.url` resolves to the entry's auto-route so links\n * (`Read more`, titles) work without hardcoding the collection slug.\n */\nexport function collectionEntryTokens(\n entry: CollectionEntryRecord,\n collectionSlug: string,\n categories?: CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<string, string> {\n const meta = collectionEntryMetaValues(\n entry,\n categories,\n undefined,\n timeZone,\n )\n const author = collectionEntryAuthorValues(entry)\n // The entry's OWN collection wins over the routed one (AGL-2518) — see\n // `CollectionEntryRecord.collectionSlug`. Unset on every single-collection\n // listing, which is why this changes nothing for them.\n const slug = (entry.collectionSlug ?? '').trim() || collectionSlug\n return {\n 'entry.title': entry.title ?? '',\n 'entry.excerpt': entry.excerpt ?? '',\n 'entry.body': entry.body ?? '',\n 'entry.coverImage': entry.coverImage ?? '',\n // The featured video (AGL-2956), raw like the cover: a template binds it\n // into a Video element's source, and the element resolves a reference\n // against the site rendering it.\n 'entry.coverVideo': entry.coverVideo ?? '',\n 'entry.slug': entry.slug ?? '',\n 'entry.url': `/${slug}/${entry.slug ?? ''}`,\n // Which section this entry belongs to (AGL-2518). Worth binding only on a\n // listing that MIXES collections — the author page — where a card\n // otherwise gives a reader no way to tell a release note from an essay.\n 'entry.collection': (entry.collectionName ?? '').trim(),\n 'entry.collectionSlug': slug,\n 'entry.collectionUrl': slug ? `/${slug}` : '',\n 'entry.date': meta.date,\n // The same instant for a parser (AGL-2956). `entry.date` is written for a\n // reader, so a Video element's publication date bound to it publishes an\n // `uploadDate` no crawler can read; this one is an ISO date-time.\n 'entry.publishedAt': collectionEntryPublishedAtIso(entry.publishedAt),\n // The byline (AGL-1459). Bindable by hand for the same reason every other\n // field is: a template that wants it somewhere Entry Meta does not reach.\n 'entry.author': meta.author,\n // The rest of the author RECORD (AGL-2486). Bindable by hand for the\n // same reason `entry.author` is: a card laid out by the designer rather\n // than dropped as the Entry Author block still has to say who wrote the\n // piece, and typing the blurb in as literal text is how a byline starts\n // naming the wrong person.\n 'entry.authorBio': author.bio,\n 'entry.authorImage': author.image,\n 'entry.authorUrl': author.url,\n /**\n * This author's page on this site (AGL-2518) — \"more from the person who\n * wrote this\".\n *\n * A separate token from `entry.authorUrl`, which is the author's own site,\n * because they are different destinations and a template should be able to\n * offer either or both. Empty when there is nothing addressable, so a\n * binding renders no link rather than one pointing at `/author/`.\n */\n 'entry.authorPageUrl': contentAuthorPageUrl({\n ...(entry.author ? { author: entry.author } : {}),\n ...(entry.authorId ? { authorId: entry.authorId } : {}),\n ...(entry.authorName ? { authorName: entry.authorName } : {}),\n }),\n // Entry model v2 (AGL-582): taxonomy + SEO tokens. The SEO pair falls\n // back to title/excerpt so templates can bind them unconditionally.\n 'entry.category': meta.category,\n 'entry.tags': meta.tags,\n 'entry.seoTitle': entry.seoTitle || entry.title || '',\n 'entry.seoDescription': entry.seoDescription || entry.excerpt || '',\n }\n}\n\n/**\n * Category/tag membership check (AGL-582), shared by the Collection entries\n * filter props and the query-less list surfaces. Matching is trimmed and\n * case-insensitive — editors type these by hand. A category filter matches\n * either the entry's stable `categoryId` or its RESOLVED display name (id\n * lookup first, legacy string fallback), so filters written against either\n * spelling keep working across the taxonomy migration.\n */\nexport function entryMatchesFilter(\n entry: CollectionEntryRecord,\n filter: { category?: string; tag?: string },\n categories?: CollectionCategory[],\n): boolean {\n const normalize = (value: string) => value.trim().toLowerCase()\n if (filter.category) {\n const wanted = normalize(filter.category)\n const matchesId =\n Boolean((entry.categoryId ?? '').trim()) &&\n normalize(entry.categoryId ?? '') === wanted\n const matchesName =\n normalize(resolveEntryCategoryName(entry, categories) ?? '') === wanted\n if (!matchesId && !matchesName) return false\n }\n if (filter.tag) {\n const wanted = normalize(filter.tag)\n if (!(entry.tags ?? []).some((tag) => normalize(tag) === wanted)) {\n return false\n }\n }\n return true\n}\n\n/* ── Category routes (AGL-1321) ─────────────────────────────────────────── */\n\n/**\n * URL form of a category id or display name (AGL-1321): lowercase, runs of\n * non-alphanumerics collapsed to a single `-`, no leading/trailing dash.\n *\n * Deliberately lossy and idempotent, so the same category addresses the same\n * URL whether the author typed the taxonomy id, the display name, or the URL\n * segment itself. `Open source` → `open-source` → `open-source`.\n */\nexport function collectionCategorySlug(value: string | undefined): string {\n return String(value ?? '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/**\n * The taxonomy category a URL segment addresses (AGL-1321), matched on the\n * stable `id` OR the slugified display name — an author linking\n * `/blog/category/open-source` should not have to know which of the two the\n * data happens to store, and an `id` that survives a rename is exactly why\n * the taxonomy has one. `undefined` = the segment names no known category;\n * the caller renders an empty listing rather than crashing.\n */\nexport function resolveCollectionCategoryBySlug(\n categories: readonly CollectionCategory[] | undefined,\n slug: string | undefined,\n): CollectionCategory | undefined {\n const wanted = collectionCategorySlug(slug)\n if (!wanted) return undefined\n return (categories ?? []).find(\n (category) =>\n collectionCategorySlug(category.id) === wanted ||\n collectionCategorySlug(category.name) === wanted,\n )\n}\n\n/**\n * The canonical URL of a collection listing (AGL-1321). ONE builder, because\n * the pills, the pager, the `<link rel=\"canonical\">`, the JSON-LD and the\n * sitemap must all agree on the shape or the filter quietly becomes a\n * duplicate-content generator.\n *\n * The unfiltered page-1 listing is the bare `/{collection}` — there is no\n * `/{collection}/category/all` and no `?category=all`, so \"All\" cannot become\n * a second address for the page that already exists.\n */\nexport function collectionListUrl(options: {\n collectionSlug: string\n categorySlug?: string | null\n page?: number | null\n /** Continue after this entry's document id — the older direction. */\n after?: string | null\n /** Continue before this entry's document id — the newer direction. */\n before?: string | null\n}): string {\n const scoped = options.categorySlug\n ? `/${options.collectionSlug}/${COLLECTION_CATEGORY_ROUTE_SEGMENT}/` +\n collectionCategorySlug(options.categorySlug)\n : `/${options.collectionSlug}`\n\n /*\n * A cursor address (AGL-3219) wins over a page number when both are given,\n * because it is the one that survives a publish at the head of the list.\n */\n const cursor = (options.before || options.after || '').trim()\n if (cursor) {\n const segment = options.before\n ? COLLECTION_BEFORE_ROUTE_SEGMENT\n : COLLECTION_AFTER_ROUTE_SEGMENT\n return `${scoped}/${segment}/${encodeURIComponent(cursor)}`\n }\n\n /*\n * `/{collection}/page/{n}` is still MINTED for a collection that fits\n * inside one read, where it is exactly as stable as it ever was: every one\n * of those pages is sliced out of a single cached snapshot, so no publish\n * can move one page's entries relative to another's. It is past the bound —\n * where the head and the tail were two different reads of a moving list —\n * that a number stopped being an address, and there the pager mints\n * cursors instead.\n */\n const page = Number(options.page)\n return Number.isFinite(page) && page > 1\n ? `${scoped}/${COLLECTION_PAGE_ROUTE_SEGMENT}/${Math.floor(page)}`\n : scoped\n}\n\n/** Where a listing's pager can go from here (AGL-1386). */\nexport interface CollectionPaginationLinks {\n /** 1-based current page; 1 when the listing is unpaginated. */\n page: number\n /**\n * Total pages in the CURRENT set.\n *\n * @deprecated AGL-3219 — 0 when it cannot be known, which is any collection\n * past one read's bound. See `CollectionPagination.totalPages`.\n */\n totalPages: number\n /** The previous page's URL, or `''` on the first page. */\n prevUrl: string\n /** The next page's URL, or `''` when nothing older follows. */\n nextUrl: string\n}\n\n/**\n * Everything a pager needs for one listing (AGL-1386): which page it is on,\n * how many there are, and where prev/next go — through\n * {@link collectionListUrl}, so both links stay inside the category they are\n * paging rather than dumping the reader back onto the unfiltered list.\n *\n * **The edges resolve to the empty string, never to a URL.** No previous page\n * means `prevUrl` is `''`; no next page means `nextUrl` is `''`. That is what\n * lets an authored template bind these unconditionally: one static list screen\n * serves the bare listing, every `/page/{n}`, and every\n * `/category/{slug}` — and there is no runtime conditional to hide a link\n * with. A link whose href resolves to `''` renders as an inert placeholder of\n * the same element (AGL-1268/1357), which is the correct pager on page 1 of 1.\n *\n * ONE computation, because it has two consumers that must agree: the built-in\n * fallback pager's nodes and the `{{pagination.*}}` tokens an authored\n * template binds. A second derivation is how a fallback and a template start\n * disagreeing about which page they are on.\n */\nexport function collectionPaginationLinks(options: {\n collectionSlug: string\n categorySlug?: string | null\n /** 1-based current page; anything unusable reads as page 1. */\n page?: number | null\n /**\n * Total pages.\n *\n * @deprecated AGL-3219 — no longer consulted for whether a next link\n * exists; {@link collectionPaginationLinks} asks `nextCursor` instead.\n */\n totalPages?: number | null\n /**\n * The document id the older page continues from, or `''` when there is no\n * older page (AGL-3219).\n *\n * This is the ONLY thing that decides whether `nextUrl` is a link. It comes\n * from a `limit(perPage + 1)` probe, so an empty string means the read\n * looked for an eleventh entry and there was not one — which is a fact\n * about the collection, unlike a total that two differently-cached reads\n * could disagree about.\n */\n nextCursor?: string | null\n /** The document id the newer page continues before, or `''`. */\n prevCursor?: string | null\n}): CollectionPaginationLinks {\n const positive = (value: number | null | undefined): number => {\n const parsed = Math.floor(Number(value))\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 1\n }\n const page = positive(options.page)\n const totalPagesRaw = Math.floor(Number(options.totalPages))\n /*\n * Zero means UNKNOWN, and only a caller that deals in cursors can mean it\n * (AGL-3219).\n *\n * A listing past the bound passes cursors and no total, and 0 is how it\n * says the number cannot be had. A caller that passes neither — an entry\n * route, which has no listing at all — is the inert case the tokens have\n * always answered with \"page 1 of 1\", and it keeps that answer rather than\n * telling an entry template it is on page 1 of 0.\n */\n const knowsCursors =\n options.nextCursor !== undefined || options.prevCursor !== undefined\n const totalPages =\n Number.isFinite(totalPagesRaw) && totalPagesRaw > 0\n ? totalPagesRaw\n : knowsCursors\n ? 0\n : 1\n const scope = {\n collectionSlug: options.collectionSlug,\n ...(options.categorySlug ? { categorySlug: options.categorySlug } : {}),\n }\n const next = (options.nextCursor ?? '').trim()\n const prev = (options.prevCursor ?? '').trim()\n\n /*\n * Numbered while the whole set fits inside ONE read, cursors once it does\n * not (AGL-3219).\n *\n * A total is only ever stated for a collection the shared read holds\n * entirely, and there every page is sliced out of that one cached snapshot:\n * no publish can move one page's entries relative to another's, so a number\n * is a perfectly good address and stays the one readers and crawlers\n * already have. Past the bound the head and the tail were two reads of a\n * moving list, a number stopped naming the same ten entries from one render\n * to the next, and the pager mints the cursor its own page proves instead.\n */\n const numbered = totalPages > 0\n const numberedHref = (n: number) => collectionListUrl({ ...scope, page: n })\n\n return {\n page,\n totalPages,\n /*\n * Page 2 going back lands on the bare listing either way: it is the head\n * of the collection, and `/{collection}` is its address — there is no\n * cursor for \"the beginning\".\n */\n prevUrl:\n page === 2\n ? collectionListUrl(scope)\n : page > 2\n ? numbered\n ? numberedHref(page - 1)\n : prev\n ? collectionListUrl({ ...scope, before: prev })\n : ''\n : '',\n nextUrl: numbered\n ? page < totalPages\n ? numberedHref(page + 1)\n : ''\n : next\n ? collectionListUrl({ ...scope, after: next })\n : '',\n }\n}\n\n/** A content-collection path resolved to what it addresses (AGL-1321). */\nexport interface CollectionRoute {\n collectionSlug: string\n /** Set only for `/{collection}/{entry}`. */\n entrySlug?: string\n /** Set only for `/{collection}/category/{slug}[/page/{n}]`. */\n categorySlug?: string\n /** 1-based list page; always 1 for entry routes. */\n page: number\n /** Set only for `/{collection}/after/{cursor}` (AGL-3219). */\n after?: string\n /** Set only for `/{collection}/before/{cursor}` (AGL-3219). */\n before?: string\n}\n\nconst POSITIVE_INTEGER = /^[1-9]\\d*$/\n\n/**\n * Parses a non-screen path against the content-collection route shapes\n * (AGL-81/620/1321), or `null` when it is none of them and the caller should\n * fall through to the 404 chain:\n *\n * ```\n * /{collection} list, page 1, all categories\n * /{collection}/{entry} one entry\n * /{collection}/page/{n} list page n\n * /{collection}/category/{slug} filtered list, page 1\n * /{collection}/category/{slug}/page/{n} filtered list, page n\n * ```\n *\n * Pure, and shared by the tenant loader and its tests, because the route\n * table IS the ISR cache key here: the tenant catch-all caches by PATH, so a\n * listing variant that lives in the path gets its own cache entry for free,\n * while one that lived in a query string would share `/blog`'s entry and\n * serve one category's HTML for another.\n */\nexport function parseCollectionRoute(\n segments: readonly string[],\n): CollectionRoute | null {\n const [collectionSlug, ...rest] = segments\n if (!collectionSlug) return null\n if (!rest.length) return { collectionSlug, page: 1 }\n if (rest.length === 1) return { collectionSlug, entrySlug: rest[0], page: 1 }\n if (rest.length === 2 && rest[0] === COLLECTION_PAGE_ROUTE_SEGMENT) {\n return POSITIVE_INTEGER.test(rest[1])\n ? { collectionSlug, page: Number(rest[1]) }\n : null\n }\n /*\n * `/{collection}/after/{cursor}` and its `before` twin (AGL-3219).\n *\n * Checked BEFORE the category shape and after the one-segment entry shape,\n * so an entry whose slug happens to be `after` still answers at\n * `/{collection}/after` — that is one segment, and this is two.\n *\n * `page` stays 1: nothing counts a cursor address, and it is the display\n * label rather than a position anything reads back.\n */\n if (\n rest.length === 2 &&\n (rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT ||\n rest[0] === COLLECTION_BEFORE_ROUTE_SEGMENT) &&\n rest[1]\n ) {\n const cursor = decodeURIComponent(rest[1]).trim()\n if (!cursor) return null\n return rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT\n ? { collectionSlug, after: cursor, page: 1 }\n : { collectionSlug, before: cursor, page: 1 }\n }\n if (rest.length === 2 && rest[0] === COLLECTION_CATEGORY_ROUTE_SEGMENT) {\n const categorySlug = collectionCategorySlug(rest[1])\n return categorySlug ? { collectionSlug, categorySlug, page: 1 } : null\n }\n if (\n rest.length === 4 &&\n rest[0] === COLLECTION_CATEGORY_ROUTE_SEGMENT &&\n rest[2] === COLLECTION_PAGE_ROUTE_SEGMENT &&\n POSITIVE_INTEGER.test(rest[3])\n ) {\n const categorySlug = collectionCategorySlug(rest[1])\n return categorySlug\n ? { collectionSlug, categorySlug, page: Number(rest[3]) }\n : null\n }\n return null\n}\n\n/**\n * Does this entry belong to the category a URL segment addresses (AGL-1321)?\n *\n * Every spelling the route could plausibly mean is slugified into one set and\n * checked against both of the entry's own spellings — the stable `categoryId`\n * and the RESOLVED display name. So `/blog/category/open-source` answers for\n * a taxonomy whose id is `opensource`, for one whose id is opaque but whose\n * name is \"Open source\", and for a legacy free-typed entry that predates the\n * taxonomy entirely. `entryMatchesFilter` cannot stand in: it compares raw\n * trimmed strings, so it would miss every multi-word category name.\n */\nexport function entryMatchesCategoryRoute(\n entry: CollectionEntryRecord,\n route: { slug: string; category?: CollectionCategory },\n categories?: readonly CollectionCategory[],\n): boolean {\n const wanted = new Set(\n [route.slug, route.category?.id, route.category?.name]\n .map((value) => collectionCategorySlug(value))\n .filter(Boolean),\n )\n if (!wanted.size) return false\n const entryId = collectionCategorySlug(entry.categoryId)\n if (entryId && wanted.has(entryId)) return true\n const entryName = collectionCategorySlug(\n resolveEntryCategoryName(entry, categories),\n )\n return Boolean(entryName) && wanted.has(entryName)\n}\n\n/** One category pill as stamped onto a Category Pills block (AGL-1321). */\nexport interface CollectionCategoryLink {\n label: string\n href: string\n /** The pill for the listing currently being rendered. */\n active: boolean\n}\n\n/** Label the unfiltered pill carries when the author never named one. */\nexport const COLLECTION_ALL_PILL_DEFAULT = 'All'\n\n/**\n * The value that persists \"no All pill\" (AGL-1336).\n *\n * The attribute documented \"clear it to omit that pill\" and could not do it:\n * the attributes form strips an emptied text field's KEY (ddf maps an\n * emptied field to its `clearedValue`, final-form's default parse turns\n * `''` into `undefined`, and `updateNodeProps` replaces the props object),\n * so the absent key took {@link COLLECTION_ALL_PILL_DEFAULT} and the pill\n * came straight back. `''` is therefore not a value this field can hold —\n * the same AGL-1191 trap that made the App Bar's \"Default\" unpersistable.\n *\n * So the field carries this as its `clearedValue`: clearing the box writes\n * the word `none`, which survives the round trip and is honoured here.\n * Deliberately a readable word rather than a magic character — an author who\n * reopens the panel sees `none` in the All-label box and can read what it\n * did. Removing the prop entirely (the ✕ affordance) still means \"unset\",\n * and restores the default.\n */\nexport const COLLECTION_ALL_PILL_NONE = 'none'\n\n/**\n * What the unfiltered pill should be labelled, or `''` to omit it — the ONE\n * place the {@link COLLECTION_ALL_PILL_NONE} sentinel is interpreted\n * (AGL-1336). Matched case-insensitively on the trimmed value, because the\n * sentinel is typed by a human only when they are re-entering it by hand.\n *\n * `undefined` is UNSET and takes the default. `null` is CLEARED and omits —\n * it reaches here only from a hand-edited or imported document, but the two\n * are different questions and must not collapse into one answer.\n */\nexport function resolveCollectionAllLabel(value?: string | null): string {\n if (value === undefined) return COLLECTION_ALL_PILL_DEFAULT\n if (value === null) return ''\n const text = `${value}`.trim()\n if (!text) return ''\n return text.toLowerCase() === COLLECTION_ALL_PILL_NONE ? '' : text\n}\n\n/**\n * The pill row for a collection (AGL-1321): \"All\" pointing at the canonical\n * unfiltered listing, then one pill per taxonomy category.\n *\n * Each pill addresses its category by the STABLE id where there is one, so a\n * rename moves the label without breaking the link. Active state is decided\n * through the same resolver the route used, not by string equality, because\n * the URL may legitimately have named the category by either spelling.\n */\nexport function buildCollectionCategoryLinks(options: {\n collectionSlug: string\n categories?: readonly CollectionCategory[]\n activeCategorySlug?: string\n /**\n * Label for the unfiltered pill. Absent takes the default; `''` or the\n * {@link COLLECTION_ALL_PILL_NONE} sentinel omits the pill.\n */\n allLabel?: string | null\n}): CollectionCategoryLink[] {\n const { collectionSlug } = options\n const active = collectionCategorySlug(options.activeCategorySlug)\n const links: CollectionCategoryLink[] = []\n for (const category of options.categories ?? []) {\n const idSlug = collectionCategorySlug(category.id)\n const nameSlug = collectionCategorySlug(category.name)\n const slug = idSlug || nameSlug\n if (!slug) continue\n links.push({\n label: category.name,\n href: collectionListUrl({ collectionSlug, categorySlug: slug }),\n active: Boolean(active) && (idSlug === active || nameSlug === active),\n })\n }\n // A lone \"All\" pill is not a filter, it is decoration — a collection with\n // no taxonomy gets no row at all.\n if (!links.length) return []\n const allLabel = resolveCollectionAllLabel(options.allLabel)\n return allLabel\n ? [\n {\n label: allLabel,\n href: collectionListUrl({ collectionSlug }),\n active: !active,\n },\n ...links,\n ]\n : links\n}\n\n/**\n * The matchable text a search box holds for a set of entries (AGL-1516),\n * built ONCE for both blocks that carry one — the entries block's in-place\n * filter and the standalone toolbar box (AGL-1516, Figma 494:1220).\n *\n * Shared rather than copied for the same reason\n * {@link COLLECTION_SEARCH_FUSE_OPTIONS} is: the two boxes can sit on the\n * same page, over the same collection, and a reader who sees a suggestion in\n * one and a miss in the other has been told the site is broken. Two builders\n * is how the row's date, chip and link drift apart.\n *\n * The row's link, date and category are resolved HERE, where the entry still\n * exists — a component only holds rendered markup by the time it runs, and\n * the suggestion panel draws its rows from this index rather than from the\n * clones. Through the same three resolvers Related posts uses (AGL-1926 for\n * the date especially): a suggestion for a post is the same post as the card\n * below it and must not be able to disagree with it about its date.\n */\nexport function buildCollectionSearchIndex(\n entries: readonly CollectionEntryRecord[],\n collectionSlug: string,\n categories?: readonly CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): CollectionEntrySearchItem[] {\n return entries.map((entry): CollectionEntrySearchItem => {\n const categoryName = resolveEntryCategoryName(entry, categories)\n return {\n title: entry.title ?? '',\n excerpt: entry.excerpt ?? '',\n // Absent keys rather than empty strings, so the row asks one question\n // per field instead of two — the shape Related posts settled on\n // (AGL-1457).\n // The entry's own collection wins over the passed one, as in\n // `collectionEntryTokens` and for the same reason (AGL-2518): a mixed\n // listing's search index must not send every hit to one collection.\n ...(entry.slug\n ? {\n url: `/${(entry.collectionSlug ?? '').trim() || collectionSlug}/${\n entry.slug\n }`,\n }\n : {}),\n ...(entry.publishedAt?.seconds\n ? {\n date: formatCollectionEntryDate(\n entry.publishedAt,\n undefined,\n undefined,\n timeZone,\n ),\n }\n : {}),\n ...(categoryName ? { category: categoryName } : {}),\n }\n })\n}\n\n/**\n * The collection blocks that can be BOUND to a collection by name, rather\n * than inheriting the routed one.\n *\n * The same three `scanCollectionBlocks` resolves a source for, and the same\n * three that can therefore put a collection's entries on a page that is not\n * one of the collection's own routes — a \"Latest posts\" rail on the home\n * page, a category pill strip in a layout's chrome, a search box in a header.\n *\n * Related posts is deliberately absent. It resolves against the routed\n * collection and renders nothing without a current entry in context, so it\n * only ever appears on an entry page, which the entry's own address already\n * covers.\n */\nconst COLLECTION_BOUND_COMPONENT_IDS: readonly string[] = [\n COLLECTION_ENTRIES_COMPONENT_ID,\n COLLECTION_CATEGORIES_COMPONENT_ID,\n COLLECTION_SEARCH_COMPONENT_ID,\n]\n\n/**\n * Does this tree render the collection `collectionSlug` away from the\n * collection's own routes?\n *\n * The content twin of {@link nodesPlaceForm}, and it exists for the same\n * reason: an entry edit changes every page that renders the collection, and\n * those pages are found by SEARCHING node trees rather than by matching a\n * pointer. A collection's own addresses are derivable from its slug; a rail\n * on the home page is not derivable from anything.\n *\n * Matches only an EXPLICIT `collectionSlug`. A block that names none inherits\n * the routed collection — which means it renders nothing at all off-route\n * (`scanCollectionBlocks` adds no slug, so the block is left untouched) and\n * renders the routed collection on-route, where the collection's own list and\n * entry addresses already reach it. Treating an unbound block as a dependent\n * of every collection on the site would make every entry save drop every page\n * that carries one.\n *\n * The comparison is against the slug rather than the document id because that\n * is what the binding stores, and what `scanCollectionBlocks` resolves. A\n * collection whose slug was just changed is a different question, and one the\n * slug change itself has to answer.\n */\nexport function nodesRenderCollection(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n collectionSlug: string,\n): boolean {\n const wanted = String(collectionSlug ?? '').trim()\n if (!wanted) return false\n for (const node of Object.values(nodes ?? {})) {\n if (!node) continue\n if (!COLLECTION_BOUND_COMPONENT_IDS.includes(String(node.componentId))) {\n continue\n }\n const bound = (node.props as Record<string, unknown> | undefined)?.[\n 'collectionSlug'\n ]\n if (typeof bound === 'string' && bound.trim() === wanted) return true\n }\n return false\n}\n\n/**\n * Collection entries blocks (AGL-551): a `collectionEntries` container\n * treats its children as the item template and renders them once per\n * published entry, with `{{entry.*}}` tokens in cloned string props replaced\n * per entry — the content-collection sibling of {@link expandRepeatables}.\n *\n * - The block's collection resolves from its `collectionSlug` prop, falling\n * back to `defaultSlug` (the routed collection on list-template screens).\n * - Clone ids are namespaced `centry__{containerId}__{index}__…` so repeats\n * never collide; run AFTER grafting and BEFORE binding resolution.\n * - Rows are bounded by `props.entriesLimit` and\n * {@link COLLECTION_ENTRIES_MAX}.\n * - `props.firstPageOnly` renders zero rows past page 1 of the ROUTED\n * listing (AGL-1871), so a lead card does not repeat down the pagination.\n * - An UNKNOWN collection leaves the node untouched (fail-open: a renamed\n * collection must never take a screen down). A known collection with no\n * matching entries renders zero rows — never the unsubstituted template.\n * - Inputs are never mutated; template nodes stay in the map unreferenced.\n */\nexport function expandCollectionEntries<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n sourcesBySlug: Record<string, CollectionEntriesSource | undefined>,\n defaultSlug?: string,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n const containers = Object.entries(nodes).filter(\n ([, node]) => node?.componentId === COLLECTION_ENTRIES_COMPONENT_ID,\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const slug =\n String((container.props as any)?.collectionSlug ?? '').trim() ||\n defaultSlug\n const source = slug ? sourcesBySlug[slug] : undefined\n // An UNKNOWN collection leaves the node untouched — fail-open, so a\n // renamed collection never takes a published screen down.\n //\n // A KNOWN collection with nothing to show is a different question, and\n // used to get the same answer (AGL-1321). Leaving the template in place\n // renders it once, with its `{{entry.title}}` / `{{entry.url}}` tokens\n // never substituted, so the page shipped a ghost row of literal braces.\n // That was rare while the only way to empty a list was to publish an\n // empty collection; a category filter makes it reachable from any pill\n // whose category has no posts yet. Zero entries now means zero rows.\n if (!source) continue\n const limitRaw = Number((container.props as any)?.entriesLimit)\n const limit =\n Number.isFinite(limitRaw) && limitRaw > 0\n ? Math.min(limitRaw, COLLECTION_ENTRIES_MAX)\n : COLLECTION_ENTRIES_MAX\n\n // Pagination (AGL-620): when `perPage` is set the block renders one page\n // window `[(page-1)*perPage, page*perPage)`; without it, the legacy\n // `entriesLimit` slice from the top applies unchanged.\n const perPageRaw = Number((container.props as any)?.perPage)\n const perPage =\n Number.isFinite(perPageRaw) && perPageRaw > 0\n ? Math.min(Math.floor(perPageRaw), COLLECTION_ENTRIES_MAX)\n : undefined\n // The block's own `page` wins; otherwise the ROUTE's page applies, so\n // `/blog/page/2` and `/blog/category/guides/page/2` render page 2 of a\n // template listing instead of silently re-serving page 1 at a second URL\n // (AGL-1321). Only ever the routed collection: `source.page` is set\n // nowhere else.\n const pageRaw = Number((container.props as any)?.page)\n const routedPage = Number(source.page)\n const page =\n Number.isFinite(pageRaw) && pageRaw >= 1\n ? Math.floor(pageRaw)\n : Number.isFinite(routedPage) && routedPage >= 1\n ? Math.floor(routedPage)\n : 1\n // \"Only on page 1\" (AGL-1871): a LEAD block takes the top of the set and\n // has no page window of its own, so on `/blog/page/2` it repeats exactly\n // what page 1 led with — same cover, same title, same excerpt — above a\n // page of different posts, under an eyebrow that reads \"Latest\". True on\n // page 1, false everywhere after it.\n //\n // Opt-in and default off. The alternative reading — \"a block with no\n // `perPage` on a paginated route is a lead block\" — is wrong: a \"popular\n // posts\" rail is the same shape and belongs on every page of the listing.\n // Only the author knows which one they built, so the author says so.\n //\n // Gated on the ROUTE's page, never on the block's own pinned `page`. A\n // block that names its own page is a deliberately fixed window (\"the\n // entries page 3 would show, here\"), which says nothing about which page\n // of the listing the reader is on; and off a routed listing entirely\n // (a blog rail on the homepage) `source.page` is unset, so the switch\n // correctly hides nothing.\n const firstPageOnly = Boolean((container.props as any)?.firstPageOnly)\n const suppressedBeyondFirstPage =\n firstPageOnly && Number.isFinite(routedPage) && routedPage > 1\n\n const templateIds = Array.isArray(container.nodes)\n ? (container.nodes as NodeId[])\n : []\n if (!templateIds.length) continue\n\n // Category/tag filter props (AGL-582): the block-level answer to\n // /blog?tag=x — query params never reach the ISR-cached loader, so\n // filtered lists are designed as filtered BLOCKS instead.\n const filterCategory = String(\n (container.props as any)?.filterCategory ?? '',\n ).trim()\n const filterTag = String(\n (container.props as any)?.filterTag ?? '',\n ).trim()\n const filtered =\n filterCategory || filterTag\n ? source.entries.filter((entry) =>\n entryMatchesFilter(\n entry,\n {\n category: filterCategory || undefined,\n tag: filterTag || undefined,\n },\n source.categories,\n ),\n )\n : source.entries\n // Whether the READ behind `source.entries` reached its own bound\n // (AGL-1516). Taken off the raw set, never off `filtered`: a block-level\n // category filter legitimately narrows a complete read, and a narrowed\n // set is not a bounded one. The source's own `reachedBound` wins over\n // counting it, because the count cannot see a read that came back full\n // and was then thinned by the liveness gate.\n const sourceCapped = collectionSourceIsBounded(source)\n\n // Where this source's first entry sits in the collection (AGL-3213):\n // zero for a whole read, and the page's own offset for a windowed one.\n const windowStartRaw = Number(source.windowStart)\n const windowStart =\n Number.isFinite(windowStartRaw) && windowStartRaw > 0\n ? Math.floor(windowStartRaw)\n : 0\n const pageStart = perPage\n ? Math.max(0, (page - 1) * perPage - windowStart)\n : 0\n const windowed = suppressedBeyondFirstPage\n ? []\n : perPage\n ? filtered.slice(pageStart, pageStart + perPage)\n : filtered.slice(0, limit)\n\n const childIds: NodeId[] = []\n windowed.forEach((entry, index) => {\n const prefix = `${COLLECTION_ENTRIES_NODE_ID_PREFIX}${containerId}__${index}__`\n const prefixId = (id: NodeId) => `${prefix}${id}`\n const cloned: Record<NodeId, N> = {}\n const cloneSubtree = (id: NodeId, parentId: NodeId) => {\n const node = nodes[id]\n if (!node) return\n const clonedChildren = Array.isArray(node.nodes)\n ? (node.nodes as NodeId[])\n : undefined\n cloned[prefixId(id)] = {\n ...node,\n $id: prefixId(id),\n parentId,\n ...(clonedChildren && {\n nodes: clonedChildren.map((childId) => prefixId(childId)),\n }),\n }\n clonedChildren?.forEach((childId) =>\n cloneSubtree(childId, prefixId(id)),\n )\n }\n for (const templateId of templateIds) {\n cloneSubtree(templateId, containerId)\n childIds.push(prefixId(templateId))\n }\n // Fill the entry blocks in THIS card from THIS card's entry (AGL-2486).\n //\n // The routed-entry passes (`expandCollectionEntryMeta`,\n // `expandCollectionEntryAuthor`) skip clones deliberately: stamping the\n // routed entry into a listing would date every card the same. That left\n // a cloned block with only its `{{entry.*}}` bindings, and a binding\n // carries no FORMAT — so an Entry Meta inside a card rendered the locale\n // default whatever its Date format said, while the identical block on\n // the article above it obeyed the picker. A field that cannot move the\n // thing it names is worse than a missing one.\n //\n // The fill runs against each clone's own entry, so every card dates\n // itself, and it runs BEFORE substitution so a filled value is already\n // a literal by the time the tokens resolve. The precedence is the\n // shared one: an authored value wins, except the `{{entry.date}}`\n // binding under a chosen format, which is the case this exists for.\n for (const [cloneId, cloneNode] of Object.entries(cloned)) {\n const fill =\n cloneNode?.componentId === COLLECTION_ENTRY_META_COMPONENT_ID\n ? entryMetaFill(cloneNode, entry, source.categories, timeZone)\n : cloneNode?.componentId === COLLECTION_ENTRY_AUTHOR_COMPONENT_ID\n ? entryAuthorFill(cloneNode, entry)\n : undefined\n if (!fill || !Object.keys(fill).length) continue\n cloned[cloneId] = {\n ...cloneNode,\n props: { ...(cloneNode.props ?? {}), ...fill },\n } as N\n }\n Object.assign(\n next,\n resolveNamedTokens(\n cloned,\n collectionEntryTokens(\n entry,\n source.slug,\n source.categories,\n timeZone,\n ),\n ),\n )\n })\n // Search (AGL-1516): the block's search box filters CLIENT-SIDE over the\n // entries this expansion just rendered — the ISR-cached page is the whole\n // data set, so no query ever goes back to Firestore. The component only\n // holds cloned markup by render time, so the matchable text is stamped\n // here, where the entries still exist, index-aligned with the clone\n // groups. Stamped ONLY when the author enabled it: an untouched block's\n // node — and its serialized payload — stays byte-identical.\n const searchEnabled = Boolean((container.props as any)?.search)\n next[containerId] = searchEnabled\n ? {\n ...container,\n props: {\n ...(container.props as any),\n searchIndex: buildCollectionSearchIndex(\n windowed,\n source.slug,\n source.categories,\n timeZone,\n ),\n // How many entries the window above was drawn FROM (AGL-1516).\n //\n // The component has to tell the reader whether a miss means \"not\n // here\" or \"not anywhere\", and it cannot work that out for\n // itself: it sees `perPage`, but `perPage` is only one of three\n // ways this block gets truncated. `entriesLimit` slices too, and\n // so does COLLECTION_ENTRIES_MAX at 100. A block set to \"latest 6\n // posts\" indexes 6 of 40 and, keyed off `perPage` alone, told the\n // reader flatly that their post does not exist.\n //\n // Stamped as the COUNT rather than as a `truncated` verdict, so\n // the component decides the wording and this stays a fact. It is\n // the post-filter total — a category-filtered block searches its\n // category, and \"the rest of the collection\" means the rest of\n // what this block would otherwise show.\n searchTotal: filtered.length,\n // And whether `searchTotal` is itself a ceiling (AGL-1516).\n //\n // `source.entries` is a READ, and that read is bounded — the\n // tenant fetches a collection's live entries with a hard\n // `.limit(100)` and no `orderBy`. On a collection past that bound\n // `filtered.length` reports the bound, not the collection, so\n // `searchIndex.length === searchTotal` — the test the component\n // reads as \"this block holds the whole set\" — is satisfied by a\n // block that holds 100 of 400 posts. The completeness claim is\n // the one thing a truncated read must never be allowed to make:\n // \"no matches\" over a quarter of a collection is a confident lie\n // about the other three quarters.\n //\n // A fact, like the count beside it: \"the read that produced this\n // index reached its bound\". A collection of exactly 100 sets it\n // too, and is described one notch more cautiously than it needed\n // to be — which is the safe direction, and the only one available\n // without a second count query.\n ...(sourceCapped ? { searchCapped: true } : {}),\n },\n nodes: childIds,\n }\n : { ...container, nodes: childIds }\n }\n return next\n}\n\n/**\n * Category Pills blocks (AGL-1321): stamps each `collectionCategories` node\n * with its collection's pill row as a serializable `items` prop the component\n * renders as real anchors — the same server-stamped shape Related posts uses,\n * and for the same reason: the block owns its markup, so there is no template\n * to clone.\n *\n * `activeCategorySlug` marks a pill only on the ROUTED collection. A pills\n * block pointing at some other collection is not what this URL filters, so\n * marking one of its pills current would be a lie in the HTML.\n *\n * A collection with no taxonomy leaves its block untouched, so the component's\n * besigner affordance / empty site render applies. Inputs are never mutated.\n */\nexport function expandCollectionCategories<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n sourcesBySlug: Record<string, CollectionEntriesSource | undefined>,\n defaultSlug?: string,\n activeCategorySlug?: string,\n): Record<NodeId, N> {\n const containers = Object.entries(nodes).filter(\n ([, node]) => node?.componentId === COLLECTION_CATEGORIES_COMPONENT_ID,\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const slug =\n String((container.props as any)?.collectionSlug ?? '').trim() ||\n defaultSlug\n const source = slug ? sourcesBySlug[slug] : undefined\n if (!source) continue\n const allLabel = (container.props as any)?.allLabel\n const items = buildCollectionCategoryLinks({\n collectionSlug: source.slug,\n categories: source.categories,\n ...(slug === defaultSlug ? { activeCategorySlug } : {}),\n // Absent means \"never set\" and takes the default; `null` is a cleared\n // value and is forwarded AS null, because `String(null)` would have\n // labelled the pill \"null\" (AGL-1336).\n ...(allLabel === undefined\n ? {}\n : { allLabel: allLabel === null ? null : String(allLabel) }),\n })\n if (!items.length) continue\n next[containerId] = {\n ...container,\n props: { ...(container.props as any), items },\n }\n }\n return next\n}\n\n/**\n * Collection Search blocks (AGL-1516): stamps each `collectionSearch` node\n * with its collection's search index, the size of the set that index was\n * drawn from, and whether that set was a bounded read — the same\n * server-stamped shape Category Pills and Related posts use, and for the same\n * reason: the block owns its markup, so there is no template to clone.\n *\n * Indexes the WHOLE source rather than a page window. The entries block's box\n * searches the entries that block rendered because it filters them in place;\n * a toolbar box filters nothing and suggests, so scoping it to one page of a\n * listing would be an arbitrary cut nobody asked for. What it can honestly\n * cover is the set the page's listing is drawn from — which on a category\n * route is that category, because `source.entries` arrives filtered.\n *\n * Clones are SKIPPED by their `centry__` prefix: a search box inside an entry\n * template would otherwise be stamped once per card. Nodes are never mutated,\n * and a collection with no entries leaves its block untouched, so the\n * component's besigner affordance and its empty site render both stand.\n */\nexport function expandCollectionSearch<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n sourcesBySlug: Record<string, CollectionEntriesSource | undefined>,\n defaultSlug?: string,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n const containers = Object.entries(nodes).filter(\n ([id, node]) =>\n node?.componentId === COLLECTION_SEARCH_COMPONENT_ID &&\n !id.startsWith(COLLECTION_ENTRIES_NODE_ID_PREFIX),\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const slug =\n String((container.props as any)?.collectionSlug ?? '').trim() ||\n defaultSlug\n const source = slug ? sourcesBySlug[slug] : undefined\n // An unknown collection leaves the node untouched — fail-open, exactly as\n // the entries and pills blocks do, so a rename never takes a screen down.\n if (!source) continue\n // A collection with nothing published gets no box. A search field over an\n // empty index can only ever answer \"no matches\", which reads as a fact\n // about the reader's query rather than about the empty collection it\n // really describes.\n if (!source.entries.length) continue\n next[containerId] = {\n ...container,\n props: {\n ...(container.props as any),\n searchIndex: buildCollectionSearchIndex(\n source.entries,\n source.slug,\n source.categories,\n timeZone,\n ),\n searchTotal: source.entries.length,\n ...(collectionSourceIsBounded(source) ? { searchCapped: true } : {}),\n },\n }\n }\n return next\n}\n\n/**\n * Entry Meta blocks (AGL-1385): stamps each `collectionEntryMeta` node with\n * the routed entry's date, category and tags — the same server-stamped shape\n * Category Pills and Related posts already use.\n *\n * MEASURED on the live marketing blog, which is what this fixes. The block was\n * on `blogEntryTmpl` carrying props `{showDate: true, showCategory: true,\n * showTags: true}` and NOTHING else, and rendered as an empty `<div>` at height\n * 0 — while the very same entry's `<pubDate>` and `Guides` category appeared in\n * its feed item, its JSON-LD, and a heading two nodes above it. So the data was\n * never missing; the block simply had nothing bound to it.\n *\n * The design is what made that reachable. Every other collection block sources\n * itself; this one required the author to type `{{entry.date}}` into a text\n * field. Only the PRESET seeds those tokens — drag the bare component out of\n * the palette and you get three switches labelled \"Show date / Show category /\n * Show tags\", all defaulting to on, gating values that can never exist. That is\n * an affordance promising something the component cannot deliver, so this is a\n * defect in the block and not in anyone's content, and it hits every collection\n * that uses it, not just blog.\n *\n * Deliberately narrow:\n *\n * - An AUTHORED value always wins. Only an absent or blank prop is filled, so\n * a hand-typed byline, a pinned string, or a `{{entry.*}}` token still\n * awaiting substitution is never overwritten. (Blank counts as unset\n * because a cleared text field cannot persist `''` anyway — AGL-1336. The\n * way to hide a value is the switch that exists for it.)\n * - Nodes cloned per entry by {@link expandCollectionEntries} are SKIPPED by\n * their `centry__` id prefix. Their tokens resolve per clone; stamping the\n * routed entry into them would date every card in a listing the same.\n * - Without a routed entry — a list route, a plain screen — nothing is\n * stamped, so the besigner affordance and the empty site render stand.\n *\n * `dateFormat` (AGL-1459) is answered here, where the timestamp still exists,\n * and is the ONE exception to \"an authored value always wins\" — narrowly, and\n * only for the literal `{{entry.date}}` binding. That binding is what the\n * block's own preset seeds, so a format that filled blanks only would do\n * nothing on the exact surface it was built for. The token is not a hand-typed\n * override; it NAMES the value being reformatted. A literal string an author\n * typed still wins, unchanged.\n *\n * Inputs are never mutated.\n */\nexport function expandCollectionEntryMeta<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n entry: CollectionEntryRecord | null | undefined,\n categories?: readonly CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n if (!entry) return nodes\n const containers = Object.entries(nodes).filter(\n ([id, node]) =>\n node?.componentId === COLLECTION_ENTRY_META_COMPONENT_ID &&\n !id.startsWith(COLLECTION_ENTRIES_NODE_ID_PREFIX),\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const filled = entryMetaFill(container, entry, categories, timeZone)\n if (!Object.keys(filled).length) continue\n next[containerId] = {\n ...container,\n props: { ...(container.props ?? {}), ...filled } as any,\n }\n }\n return next\n}\n\n/**\n * The values ONE Entry Meta node takes from ONE entry (AGL-2486).\n *\n * Extracted so a node inside a listing's card can be filled from the entry\n * that card is for, which is a different entry per clone and therefore\n * cannot come from the routed-entry pass above. Both callers apply the same\n * precedence, because the same block cannot mean two things depending on\n * where an author dropped it.\n *\n * Returns only the keys that change; an empty object means \"leave this node\n * exactly as authored\".\n */\nfunction entryMetaFill(\n container: { props?: unknown },\n entry: CollectionEntryRecord,\n categories?: readonly CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<string, string> {\n {\n const props = (container.props ?? {}) as Record<string, unknown>\n // Per node: the format is a prop, so two blocks on one template can read\n // their dates differently.\n const dateFormat = normalizeCollectionEntryDateFormat(props['dateFormat'])\n const values = collectionEntryMetaValues(\n entry,\n categories,\n dateFormat,\n timeZone,\n )\n const filled: Record<string, string> = {}\n for (const key of ['date', 'author', 'category', 'tags'] as const) {\n const authored = String(props[key] ?? '').trim()\n if (authored && !reformattableEntryDate(key, authored, dateFormat)) {\n continue\n }\n if (!values[key]) continue\n filled[key] = values[key]\n }\n // The byline avatar follows the BYLINE, and is the second narrow\n // exception to \"an authored value always wins\" (after `dateFormat`).\n //\n // `avatarImage` was introduced as a block-level pick because entries then\n // carried a byline STRING and no portrait anywhere, so the site's brand\n // mark chosen once on the template was the only answer available. Custom\n // authors changed that: an entry resolves to a record, and a record has\n // an `image`. Keeping the authored mark in front of it would print the\n // company logo beside a named person — the wrong face, on every post that\n // person wrote, which is worse than the generic mark it was standing in\n // for.\n //\n // Deliberately narrow, so nothing else moves: only a resolved author\n // RECORD that actually carries an image displaces the authored value. A\n // legacy string byline, an author with no portrait, and an entry with no\n // author at all all leave the template's mark exactly where it was.\n //\n // And only where a byline is actually PRINTED. This block is also the\n // product's tag row — the article frame ends on one with `Show author`,\n // `Show date` and `Show category` off and nothing but chips left — so a\n // portrait filled in unconditionally lands beside the tags, halfway down\n // the page from the byline it belongs to. The avatar is `alt=\"\"` on the\n // grounds that \"the byline names the author in text right beside it\",\n // which is exactly the thing a chips-only block does not have.\n const authorImage = String(entry.author?.image ?? '').trim()\n const bylineShown =\n props['showAuthor'] !== false &&\n Boolean(filled['author'] || String(props['author'] ?? '').trim())\n if (authorImage && bylineShown && props['showAvatar'] !== false) {\n filled['avatarImage'] = authorImage\n }\n /*\n Where the byline GOES (AGL-2519).\n\n Filled on the same terms as the avatar and for the same reason: it\n belongs to the byline, so it is only filled where a byline is actually\n printed. This block doubles as the product's tag row — the article frame\n ends on one with the author, date and category all switched off — and a\n link stamped there would have nothing to attach to.\n\n An authored value still wins, which is what `Link author` off leaves\n behind once the switch has written an explicit `false`: the switch is\n read by the renderer, and this only supplies the destination.\n */\n if (bylineShown) {\n const pageUrl = collectionEntryAuthorValues(entry).pageUrl\n if (pageUrl && !String(props['authorPageUrl'] ?? '').trim()) {\n filled['authorPageUrl'] = pageUrl\n }\n }\n return filled\n }\n}\n\n/**\n * Fill Entry Author cards from the routed entry's author record (AGL-2486),\n * on the same terms {@link expandCollectionEntryMeta} fills the byline:\n *\n * - an authored value always wins, and a blank one is a blank, not a\n * request for the default — the way to hide a field is its Show switch;\n * - per-entry clones (`centry__`) are skipped, so a listing's cards keep\n * resolving their own tokens rather than every card naming the routed\n * entry's author;\n * - without a routed entry nothing is stamped, which leaves the besigner's\n * placeholder card visible on the canvas.\n *\n * Inputs are never mutated.\n */\nexport function expandCollectionEntryAuthor<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n entry: CollectionEntryRecord | null | undefined,\n): Record<NodeId, N> {\n if (!entry) return nodes\n const containers = Object.entries(nodes).filter(\n ([id, node]) =>\n node?.componentId === COLLECTION_ENTRY_AUTHOR_COMPONENT_ID &&\n !id.startsWith(COLLECTION_ENTRIES_NODE_ID_PREFIX),\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const filled = entryAuthorFill(container, entry)\n if (!Object.keys(filled).length) continue\n next[containerId] = {\n ...container,\n props: { ...(container.props ?? {}), ...filled } as any,\n }\n }\n return next\n}\n\n/**\n * The values ONE Entry Author node takes from ONE entry (AGL-2486) — the\n * companion to {@link entryMetaFill}, extracted for the same reason: a card\n * inside a listing is filled from the entry that card is for.\n */\nfunction entryAuthorFill(\n container: { props?: unknown },\n entry: CollectionEntryRecord,\n): Record<string, unknown> {\n const values = collectionEntryAuthorValues(entry)\n const props = (container.props ?? {}) as Record<string, unknown>\n const filled: Record<string, unknown> = {}\n for (const key of ['name', 'bio', 'image', 'url', 'pageUrl'] as const) {\n if (String(props[key] ?? '').trim()) continue\n if (!values[key]) continue\n filled[key] = values[key]\n }\n // `links` is not authorable as text — a row carries a platform or a picked\n // icon, and the console's author editor is where those are chosen — so\n // there is no authored value to defer to. The record's rows are the only\n // ones there are; `Show links` is the template's control over them.\n if (values.links.length) filled['links'] = values.links\n return filled\n}\n\n/** The one binding a chosen date format may replace (AGL-1459). */\nconst ENTRY_DATE_BINDING = '{{entry.date}}'\n\n/**\n * Is this authored value the `{{entry.date}}` binding under a chosen format\n * (AGL-1459)? Anything else — a literal string, another token, the default\n * format — answers no, and the authored value stands.\n */\nfunction reformattableEntryDate(\n key: string,\n authored: string,\n dateFormat: CollectionEntryDateFormat,\n): boolean {\n return (\n key === 'date' &&\n authored === ENTRY_DATE_BINDING &&\n dateFormat !== COLLECTION_ENTRY_DATE_FORMAT_DEFAULT\n )\n}\n\n/**\n * The ONE matcher configuration behind every entry search in the product\n * (AGL-1516/AGL-1525) — the block's toolbar box, its suggestion panel, and\n * the site-wide results page the panel links to.\n *\n * Shared rather than copied because the two halves are joined by a link the\n * reader clicks: the panel's \"View all results\" hands its query to /search,\n * and if the results page matched more strictly it would answer a visible\n * suggestion with \"no results\". A typo the panel forgave — \"platfrom\" — is\n * exactly the query most likely to make that trip.\n *\n * The shape is the icon picker's (use-mdi-icons-fuzzy), tuned once for prose\n * and MEASURED before shipping: the title names the post and the excerpt\n * merely describes it, so they are weighted 0.7/0.3; the default location\n * scoring buries a legitimate mid-sentence hit in a sentence-long excerpt,\n * and the default 0.6 threshold matches \"media\" against every post on letter\n * soup, so location is ignored and the threshold tightened to 0.3.\n *\n * A plain object, not a Fuse instance: `libs/aglyn` is dependency-light by\n * design, and each consumer already imports Fuse from the vendor bundle.\n */\nexport const COLLECTION_SEARCH_FUSE_OPTIONS = {\n keys: [\n { name: 'title', weight: 0.7 },\n { name: 'excerpt', weight: 0.3 },\n ],\n includeScore: true,\n shouldSort: true,\n ignoreLocation: true,\n threshold: 0.3,\n}\n\n/**\n * One entry of a Collection Entries search index (AGL-1516): the fields the\n * block's client-side search box matches against, index-aligned with the\n * entry clones the block rendered. Stamped by {@link expandCollectionEntries}\n * only when the block's `search` prop is on; never set by hand.\n */\nexport interface CollectionEntrySearchItem {\n title: string\n excerpt: string\n /**\n * Entry permalink (AGL-1525). The `filter` mode never needs it — it hides\n * and shows clones that already carry their own links — but a suggestion\n * row IS the link, and the panel is drawn from the index, not from the\n * clones. Optional because a page cached before this shipped has an index\n * without it; the panel degrades to plain text rather than to a dead link.\n */\n url?: string\n /** Published date, through {@link formatCollectionEntryDate} (AGL-1525). */\n date?: string\n /** Category display name for the suggestion row's chip (AGL-1525). */\n category?: string\n}\n\n/** One related post as stamped onto a Related posts block (AGL-582). */\nexport interface CollectionRelatedItem {\n title: string\n url: string\n date?: string\n excerpt?: string\n /** Per-entry byline (AGL-686); falls back to the site as author. */\n authorName?: string\n category?: string\n /**\n * The entry's cover, as STORED (AGL-1457) — a `media:` reference, a legacy\n * URL, or a hotlink. Left unresolved on purpose: the block resolves it at\n * render through `resolveMediaSrc`, exactly as the Image element does, so\n * one reference keeps working across sites and CDN route changes.\n */\n coverImage?: string\n /**\n * The author's description of that cover (AGL-2418). Travels WITH\n * `coverImage`, and absent means \"fall back to the title\" rather than\n * \"silent\" — on this block the cover is the link's own content.\n */\n coverImageAlt?: string\n}\n\n/**\n * Related-post selection (AGL-582): other entries of the same collection\n * that share the current entry's category or at least one tag, newest\n * first. Pure so the compose stage and tests share one implementation. An\n * entry with no category and no tags relates to nothing — the caller\n * renders nothing rather than guessing.\n *\n * Two entries share a category when their stable `categoryId`s are equal\n * (survives renames AND deletions) or their RESOLVED display names match\n * case-insensitively — so a legacy free-typed entry still relates to a\n * migrated `categoryId` entry whose taxonomy name spells the same.\n */\nexport function selectRelatedEntries(\n entries: CollectionEntryRecord[],\n current: CollectionEntryRecord,\n limit = COLLECTION_RELATED_DEFAULT_LIMIT,\n categories?: CollectionCategory[],\n): CollectionEntryRecord[] {\n const categoryId = (current.categoryId ?? '').trim()\n const categoryName = (\n resolveEntryCategoryName(current, categories) ?? ''\n ).toLowerCase()\n const tags = (current.tags ?? []).map((tag) => tag.trim()).filter(Boolean)\n if (!categoryId && !categoryName && !tags.length) return []\n const bounded = Math.min(\n Math.max(limit, 0) || COLLECTION_RELATED_DEFAULT_LIMIT,\n COLLECTION_RELATED_MAX,\n )\n return entries\n .filter((entry) => {\n if (\n (entry.$id && entry.$id === current.$id) ||\n (entry.slug && entry.slug === current.slug)\n ) {\n return false\n }\n if (categoryId && (entry.categoryId ?? '').trim() === categoryId) {\n return true\n }\n if (\n categoryName &&\n (resolveEntryCategoryName(entry, categories) ?? '')\n .trim()\n .toLowerCase() === categoryName\n ) {\n return true\n }\n return tags.some((tag) => entryMatchesFilter(entry, { tag }))\n })\n .sort(\n (a, b) => (b.publishedAt?.seconds ?? 0) - (a.publishedAt?.seconds ?? 0),\n )\n .slice(0, bounded)\n}\n\n/**\n * Related posts blocks (AGL-582): stamps each `collectionRelated` node with\n * the current entry's related posts as a serializable `entries` prop the\n * component renders directly (no template cloning — the block owns its\n * markup). Runs only on entry renders; without an entry context the nodes\n * stay untouched, so the component's sample cards (editing surfaces) or its\n * empty site render apply. Inputs are never mutated.\n */\nexport function expandCollectionRelated<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n source: CollectionEntriesSource | undefined,\n currentEntry: CollectionEntryRecord | null | undefined,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n if (!source || !currentEntry) return nodes\n const containers = Object.entries(nodes).filter(\n ([, node]) => node?.componentId === COLLECTION_RELATED_COMPONENT_ID,\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const limitRaw = Number((container.props as any)?.limit)\n // Per node, exactly as the byline reads it (AGL-1459): the format is a\n // prop, so two rails on one template can date their cards differently.\n const dateFormat = normalizeCollectionEntryDateFormat(\n (container.props as any)?.dateFormat,\n )\n const related = selectRelatedEntries(\n source.entries ?? [],\n currentEntry,\n Number.isFinite(limitRaw) && limitRaw > 0\n ? limitRaw\n : COLLECTION_RELATED_DEFAULT_LIMIT,\n source.categories,\n )\n const items: CollectionRelatedItem[] = related.map((entry) => {\n const categoryName = resolveEntryCategoryName(entry, source.categories)\n return {\n title: entry.title ?? '',\n url: `/${source.slug}/${entry.slug ?? ''}`,\n // Through the one formatter (AGL-1926), not a second inline\n // `toLocaleDateString()`. It produced the same bytes only because\n // this runs on the server, where the implicit locale and zone happen\n // to be the ones `formatCollectionEntryDate` now pins; a related-post\n // card is the same published date as the byline above it and must not\n // be able to disagree with it.\n //\n // Answered HERE and not in the block (AGL-2486), for the reason the\n // byline's format is: by the time a date reaches the component it is\n // a formatted string, and re-parsing one is ambiguous by\n // construction — `8/9/2026` is 9 August under `en-US` and 8 September\n // under `en-GB`. The timestamp only exists on this side.\n ...(entry.publishedAt?.seconds\n ? {\n date: formatCollectionEntryDate(\n entry.publishedAt,\n dateFormat,\n undefined,\n timeZone,\n ),\n }\n : {}),\n ...(entry.excerpt ? { excerpt: entry.excerpt } : {}),\n // AGL-1457: the block owns its markup, so there is no template to\n // bind `{{entry.coverImage}}` on — the cover can only reach it as a\n // stamped field. Absent covers omit the key rather than stamping an\n // empty string, so the render asks one question, not two.\n ...(entry.coverImage ? { coverImage: entry.coverImage } : {}),\n // Only alongside a cover, and only when authored (AGL-2418) — a\n // description with no picture is dead weight on every stamped node.\n ...(entry.coverImage && (entry as any).coverImageAlt\n ? { coverImageAlt: (entry as any).coverImageAlt }\n : {}),\n ...(categoryName ? { category: categoryName } : {}),\n }\n })\n next[containerId] = {\n ...container,\n props: { ...(container.props as any), entries: items },\n }\n }\n return next\n}\n\nexport default expandCollectionEntries\n"],"names":["COLLECTION_ENTRY_DATE_FORMAT_DEFAULT","collectionEntryPublishedAtIso","formatCollectionEntryDate","normalizeCollectionEntryDateFormat","contentAuthorPageUrl","normalizeContentAuthorLinks","resolveNamedTokens","COLLECTION_ENTRIES_COMPONENT_ID","COLLECTION_ENTRY_BODY_COMPONENT_ID","COLLECTION_RELATED_COMPONENT_ID","COLLECTION_SHARE_COMPONENT_ID","COLLECTION_ENTRY_META_COMPONENT_ID","COLLECTION_ENTRY_AUTHOR_COMPONENT_ID","COLLECTION_CATEGORIES_COMPONENT_ID","COLLECTION_SEARCH_COMPONENT_ID","COLLECTION_PAGE_ROUTE_SEGMENT","COLLECTION_CATEGORY_ROUTE_SEGMENT","COLLECTION_AFTER_ROUTE_SEGMENT","COLLECTION_BEFORE_ROUTE_SEGMENT","COLLECTION_ENTRIES_NODE_ID_PREFIX","COLLECTION_ENTRIES_MAX","COLLECTION_SOURCE_MAX","collectionSourceReachedBound","entries","length","collectionSourceIsBounded","source","reachedBound","COLLECTION_LIST_PAGE_SIZE","collectionTotalPages","total","perPage","Math","max","ceil","collectionEntriesPageWindow","pagination","Number","isFinite","pageRaw","page","floor","startRaw","windowStart","start","slice","COLLECTION_RELATED_DEFAULT_LIMIT","COLLECTION_RELATED_MAX","COLLECTION_CATEGORIES_MAX","ENTRIES_MAX_PER_COLLECTION","COLLECTIONS_MAX_PER_HOST","resolveEntryCategoryName","entry","categories","categoryId","trim","match","find","category","id","name","legacy","undefined","collectionEntryMetaValues","dateFormat","timeZone","date","publishedAt","author","authorName","tags","join","collectionEntryAuthorValues","pageUrl","authorId","links","bio","image","url","collectionEntryTokens","collectionSlug","meta","slug","title","excerpt","body","coverImage","coverVideo","collectionName","seoTitle","seoDescription","entryMatchesFilter","filter","normalize","value","toLowerCase","wanted","matchesId","Boolean","matchesName","tag","some","collectionCategorySlug","String","replace","resolveCollectionCategoryBySlug","collectionListUrl","options","scoped","categorySlug","cursor","before","after","segment","encodeURIComponent","collectionPaginationLinks","positive","parsed","totalPagesRaw","totalPages","knowsCursors","nextCursor","prevCursor","scope","next","prev","numbered","numberedHref","n","prevUrl","nextUrl","POSITIVE_INTEGER","parseCollectionRoute","segments","rest","entrySlug","test","decodeURIComponent","entryMatchesCategoryRoute","route","Set","map","size","entryId","has","entryName","COLLECTION_ALL_PILL_DEFAULT","COLLECTION_ALL_PILL_NONE","resolveCollectionAllLabel","text","buildCollectionCategoryLinks","active","activeCategorySlug","idSlug","nameSlug","push","label","href","allLabel","buildCollectionSearchIndex","categoryName","seconds","COLLECTION_BOUND_COMPONENT_IDS","nodesRenderCollection","nodes","node","Object","values","includes","componentId","bound","props","expandCollectionEntries","sourcesBySlug","defaultSlug","containers","containerId","container","limitRaw","entriesLimit","limit","min","perPageRaw","routedPage","firstPageOnly","suppressedBeyondFirstPage","templateIds","Array","isArray","filterCategory","filterTag","filtered","sourceCapped","windowStartRaw","pageStart","windowed","childIds","forEach","index","prefix","prefixId","cloned","cloneSubtree","parentId","clonedChildren","$id","childId","templateId","cloneId","cloneNode","fill","entryMetaFill","entryAuthorFill","keys","assign","searchEnabled","search","searchIndex","searchTotal","searchCapped","expandCollectionCategories","items","expandCollectionSearch","startsWith","expandCollectionEntryMeta","filled","key","authored","reformattableEntryDate","authorImage","bylineShown","expandCollectionEntryAuthor","ENTRY_DATE_BINDING","COLLECTION_SEARCH_FUSE_OPTIONS","weight","includeScore","shouldSort","ignoreLocation","threshold","selectRelatedEntries","current","bounded","sort","a","b","expandCollectionRelated","currentEntry","related","coverImageAlt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/collection-entries.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 type { AglynNodeSchema, NodeId } from '../foundation'\nimport {\n COLLECTION_ENTRY_DATE_FORMAT_DEFAULT,\n type CollectionEntryDateFormat,\n collectionEntryPublishedAtIso,\n formatCollectionEntryDate,\n normalizeCollectionEntryDateFormat,\n} from './collection-entry-date'\nimport {\n type ContentAuthorLink,\n type ContentAuthorRecord,\n contentAuthorPageUrl,\n normalizeContentAuthorLinks,\n} from './content-authors'\nimport { resolveNamedTokens } from './resolve-named-tokens'\n\nexport * from './collection-entry-date'\n\n/**\n * Persisted component id of the \"Collection entries\" repeater block\n * (plugins-mui). Like `layoutSlot`, the id lives here so the tenant compose\n * pipeline can find the block without importing the component bundle.\n */\nexport const COLLECTION_ENTRIES_COMPONENT_ID = 'collectionEntries'\n\n/** Persisted component id of the markdown \"Entry body\" block (plugins-mui). */\nexport const COLLECTION_ENTRY_BODY_COMPONENT_ID = 'collectionEntryBody'\n\n/**\n * Persisted component id of the \"Related posts\" block (plugins-mui,\n * AGL-582). The compose pipeline stamps the related entries onto the node,\n * so the id lives here like the entries/body blocks.\n */\nexport const COLLECTION_RELATED_COMPONENT_ID = 'collectionRelated'\n\n/** Persisted component id of the share-bar block (plugins-mui, AGL-582). */\nexport const COLLECTION_SHARE_COMPONENT_ID = 'collectionShare'\n\n/** Persisted component id of the entry-meta block (plugins-mui, AGL-582). */\nexport const COLLECTION_ENTRY_META_COMPONENT_ID = 'collectionEntryMeta'\n\n/**\n * Persisted component id of the author card that closes an article\n * (plugins-mui, AGL-2486).\n *\n * Entry Meta prints the byline — a NAME, on one line, beside the date. The\n * author record it now resolves against carries a portrait, a bio, a job\n * title and a url, and none of those have anywhere to render: a template that\n * wanted the card the article frame draws had to type the name and the blurb\n * as literal text, which then said \"The Aglyn Team\" under a post somebody\n * else wrote and went stale the moment the author record was edited. The id\n * lives here, with the other collection blocks, so the compose pipeline can\n * fill the card without importing the bundle.\n */\nexport const COLLECTION_ENTRY_AUTHOR_COMPONENT_ID = 'collectionEntryAuthor'\n\n/**\n * Persisted component id of the \"Category Pills\" block (plugins-mui,\n * AGL-1321). Like the entries repeater, the id lives here so the tenant\n * compose pipeline can stamp the block's links without importing the bundle.\n */\nexport const COLLECTION_CATEGORIES_COMPONENT_ID = 'collectionCategories'\n\n/**\n * Persisted component id of the standalone \"Collection Search\" block\n * (plugins-mui, AGL-1516).\n *\n * The Collection Entries block grew its own search box first, and it renders\n * INSIDE that block — first child of the block's own stack. Figma 494:1220\n * puts the field in the listing's toolbar row instead, beside the RSS button\n * with the category pills opposite it, and no amount of authoring can lift a\n * block's own child out of it. The pills cannot move the other way either:\n * every child of an entries block is cloned once per entry.\n *\n * So the field became a block an author can put where the frame draws it.\n * Suggestion-panel only, by construction rather than by omission — a\n * standalone box has no clones to hide, so `filter` is a thing it genuinely\n * cannot do, and the one behaviour it does have is the toolbar behaviour the\n * frame asks for.\n */\nexport const COLLECTION_SEARCH_COMPONENT_ID = 'collectionSearch'\n\n/**\n * Reserved list sub-paths (AGL-620 `page`, AGL-1321 `category`). Both are\n * only reserved as the HEAD of a longer path, so an entry legitimately\n * slugged `page` or `category` keeps serving at `/{collection}/{entry}`.\n */\nexport const COLLECTION_PAGE_ROUTE_SEGMENT = 'page'\nexport const COLLECTION_CATEGORY_ROUTE_SEGMENT = 'category'\n/**\n * `/{collection}/after/{cursor}` — the older page (AGL-3219).\n *\n * ## Why a PATH segment and not `?after=`\n *\n * The tenant catch-all is statically rendered and ISR-cached with the URL\n * PATH as its key (`revalidate = 3600`, AGL-1152). A query string is not part\n * of that key, so every cursor address would share ONE cache entry and serve\n * whichever of them rendered first to everybody — the same reason a routed\n * category is `/{collection}/category/{slug}` rather than `?category=`. And\n * reading `searchParams` at all opts the whole catch-all out of static\n * rendering, which is the opposite of what that page is for.\n *\n * A path segment is cached per address, crawlable, and costs nothing.\n */\nexport const COLLECTION_AFTER_ROUTE_SEGMENT = 'after'\n/** `/{collection}/before/{cursor}` — the newer page (AGL-3219). */\nexport const COLLECTION_BEFORE_ROUTE_SEGMENT = 'before'\n\n/** Namespaces cloned template ids per container/entry (cf. `rep__`). */\nexport const COLLECTION_ENTRIES_NODE_ID_PREFIX = 'centry__'\n\n/** Hard bound on entries a single block renders, before `entriesLimit`. */\nexport const COLLECTION_ENTRIES_MAX = 100\n\n/**\n * Hard bound on the entries one READ of a collection returns (AGL-1516) — the\n * `.limit()` the tenant's live-entries query carries.\n *\n * Named here, beside the render bound it has always silently matched, because\n * a search index has to know the difference. The render bound is a choice\n * about a block; this one is a choice about the DATA, and a set that reaches\n * it is a set nobody has seen all of. Two 100s that happened to agree meant\n * `entries.length === 100` could be read either as \"this collection has 100\n * entries\" or \"this read stopped at 100\", and the honest empty state turns on\n * exactly that distinction.\n */\nexport const COLLECTION_SOURCE_MAX = 100\n\n/**\n * Whether a collection source is a COMPLETE read or a bounded one\n * (AGL-1516) — true when the read came back holding its own limit, which is\n * the only signal a limited query without a count leaves behind.\n *\n * Deliberately errs toward \"bounded\": a collection of exactly\n * {@link COLLECTION_SOURCE_MAX} entries answers true and gets described one\n * notch more cautiously than it needed. Overstating what a search covered is\n * the failure that renders as a confident \"nothing matched\".\n */\nexport function collectionSourceReachedBound(\n entries: readonly unknown[] | undefined,\n): boolean {\n return (entries?.length ?? 0) >= COLLECTION_SOURCE_MAX\n}\n\n/**\n * Whether the read behind a collection source was a bounded one (AGL-1516),\n * asking the READER first and only then falling back to counting.\n *\n * The count is a proxy, and it under-reports in two ways that both landed\n * after `collectionSourceReachedBound` was written:\n *\n * 1. The query takes `status in ['published', 'scheduled']` and the loader\n * then drops what is not live yet — a future `publishAt`, or since AGL-471\n * a due schedule the plan does not carry. A read that came back holding\n * its full 100 docs can therefore hand over 96 entries, and 96 does not\n * look like a ceiling. A blog with a scheduling queue is exactly the site\n * that hits this.\n * 2. A category route filters `entries` before compose ever sees them, so the\n * \"raw set\" the entries block is careful to measure is already narrowed.\n *\n * Both make the flag read FALSE on a read that really did stop at its bound,\n * and false is the direction that produces the confident \"nothing matched\"\n * this signal exists to prevent. So `reachedBound` is threaded down from the\n * query that owns the fact, and the count survives only as the fallback for a\n * source that predates it — a payload cached before this shipped, or a caller\n * that assembles a source by hand.\n */\nexport function collectionSourceIsBounded(\n source: Pick<CollectionEntriesSource, 'entries' | 'reachedBound'> | undefined,\n): boolean {\n if (source?.reachedBound) return true\n return collectionSourceReachedBound(source?.entries)\n}\n\n/** Default entries per page for a paginated collection list (AGL-620). */\nexport const COLLECTION_LIST_PAGE_SIZE = 10\n\n/**\n * Total pages for `total` entries at `perPage` per page — at least 1 so an\n * empty collection still has a page 1 (AGL-620).\n */\nexport function collectionTotalPages(total: number, perPage: number): number {\n if (perPage <= 0) return 1\n return Math.max(1, Math.ceil(total / perPage))\n}\n\n/**\n * The slice of a listing's entries that the page named by `pagination`\n * actually shows (AGL-3213).\n *\n * The loader hands the WHOLE narrowed set down to compose, because\n * {@link expandCollectionEntries} does its own windowing off the block's\n * props — a Collection entries block may carry its own `perPage`, its own\n * pinned `page`, a `firstPageOnly` switch and its own filters, none of which\n * this function can see. Slicing before compose would double-window and empty\n * every page after the first.\n *\n * Everything the loader serializes into page PROPS is downstream of compose,\n * and there the full set is not merely wasted bytes — it is a false\n * statement. The `ItemList` JSON-LD numbers its items\n * `(page - 1) * perPage + index + 1` on the premise that `entries` is the\n * window; handed all one hundred, `/changelog/page/7` published\n * `numberOfItems: 100` and positions 61 through 160, most of them past the\n * end of the collection. The legacy fallback renderer read it the same way\n * and rendered the whole list on every page.\n *\n * So: the loader keeps the full set until compose has had it, and narrows\n * HERE on the way into props, where `pagination` is the route's own answer to\n * which page this is.\n *\n * An unpaginated listing (an entry route, the RSS feed) passes no pagination\n * and gets its entries back untouched.\n */\nexport function collectionEntriesPageWindow<T>(\n entries: readonly T[],\n pagination:\n | { page?: number; perPage?: number; windowStart?: number }\n | null\n | undefined,\n): T[] {\n const perPage = Number(pagination?.perPage)\n if (!Number.isFinite(perPage) || perPage <= 0) return [...entries]\n const pageRaw = Number(pagination?.page)\n const page = Number.isFinite(pageRaw) && pageRaw >= 1 ? Math.floor(pageRaw) : 1\n // `windowStart` is where `entries` begins in the collection's own order\n // (AGL-3213). A listing past the cached read's bound is served by a window\n // read for that page alone, and slicing it from the page's absolute offset\n // would return nothing.\n const startRaw = Number(pagination?.windowStart)\n const windowStart =\n Number.isFinite(startRaw) && startRaw > 0 ? Math.floor(startRaw) : 0\n const start = Math.max(0, (page - 1) * Math.floor(perPage) - windowStart)\n return entries.slice(start, start + Math.floor(perPage))\n}\n\n/** Default/most related posts a Related posts block renders (AGL-582). */\nexport const COLLECTION_RELATED_DEFAULT_LIMIT = 3\nexport const COLLECTION_RELATED_MAX = 12\n\n/** Most categories a collection's taxonomy holds (AGL-582). */\nexport const COLLECTION_CATEGORIES_MAX = 50\n\n/**\n * One taxonomy category on a COLLECTION doc (AGL-582): `id` is the stable\n * slug-like key entries reference (`categoryId`), generated once from the\n * initial name and never changed; `name` is the display label and freely\n * renameable. Stored as an array to preserve author-defined ordering.\n */\nexport interface CollectionCategory {\n id: string\n name: string\n /**\n * What this category's listing is about, for the `<meta name=\"description\">`\n * of `/{collection}/category/{slug}`.\n *\n * Without one every filtered listing on a site inherits the list template\n * screen's description — six `/blog/category/*` URLs all described as \"the\n * blog\", which is the same duplicate-result problem the composed category\n * TITLE was introduced to solve (AGL-1321), one tag over. Optional, and the\n * template's description remains the fallback: a category nobody has\n * described is better summarized by the list it filters than by nothing.\n */\n description?: string\n}\n\n/**\n * How many LIVE entries one content collection may hold (AGL-2266).\n *\n * `hosts/{hostId}/collections/{cid}/entries/{eid}` had a DEDICATED rules block\n * re-granting `create` to any editor, client-direct — deliberately, because the\n * name-based exclusions on the host catch-all must not reach entries — so it\n * was the one quota-governed shape under a host with no quota at all. A free\n * org could mint unbounded Firestore documents from the browser.\n *\n * This is a flat PLATFORM cap, not a plan dimension. AGL-1387 declined\n * `collectionsPerHost` and this does not re-open it: nothing here is priced,\n * no `OrgEntitlements` key is added, and every plan gets the same number. It is\n * the `WEBHOOK_MAX_PER_HOST` (AGL-1360) / `NON_PAGE_SCREEN_MAX_PER_HOST`\n * (AGL-1399) shape, which both issues invented for exactly this: uncapped\n * infrastructure behind a $0 subscription.\n *\n * ## Per COLLECTION, paired with {@link COLLECTIONS_MAX_PER_HOST}\n *\n * A per-HOST entry cap would need a count across every collection the host\n * holds, which is a collection-group read the entry documents carry no `hostId`\n * to scope. Per collection is one `count()` on the collection the create is\n * addressed to — the same read every other cap on `/api/hosts/resources`\n * already pays — and the host is bounded by the PRODUCT of the two numbers,\n * which is why the sibling cap had to land in the same change. One of the two\n * alone bounds nothing: unbounded collections each holding 10,000 entries is\n * the same unbounded store, spelled differently.\n *\n * ## Why 10,000\n *\n * A blog publishing daily reaches it after twenty-seven years; the largest\n * content collection in production holds double digits. The failure mode of a\n * flat cap set too low is blocking real authoring with an error the price list\n * cannot explain, so it is sized to the heaviest plausible library rather than\n * to today's data. It also bounds the cost of the console's own listing, which\n * reads entries with `limit(200)` and would otherwise page through a store with\n * no ceiling.\n */\nexport const ENTRIES_MAX_PER_COLLECTION = 10000\n\n/**\n * How many LIVE content collections one host may hold (AGL-2266).\n *\n * The other half of the entry bound above, and the reason it means anything.\n * Collection creation was already server-owned — the rules deny client `create`\n * and `/api/hosts/collections` claims the slug transactionally (AGL-978) — so\n * the collection had a WRITER with authority and simply no number to enforce.\n *\n * Again a platform cap and not `collectionsPerHost`: AGL-1387 decided\n * collections are not a thing customers are charged for, and that decision is\n * untouched. 100 is far past any real site's taxonomy — production's busiest\n * host has a handful — and short enough that the entries ceiling above\n * multiplies out to a bounded store rather than an unbounded one.\n */\nexport const COLLECTIONS_MAX_PER_HOST = 100\n\n/**\n * One published content-collection entry as the compose pipeline sees it\n * (AGL-551). Mirrors the tenant's `CollectionEntrySummary` without the\n * Firestore dependency so the expansion stays pure.\n */\nexport interface CollectionEntryRecord {\n $id?: string\n title?: string\n slug?: string\n excerpt?: string\n /**\n * Per-entry byline (AGL-686); falls back to the site as author.\n *\n * Since AGL-2486 this is either the legacy free-typed string or the NAME of\n * the author record `authorId` points at, denormalized by the tenant runtime\n * so the byline block, the `{{entry.author}}` token and the RSS feed all\n * keep reading one field.\n */\n authorName?: string\n /**\n * Reference into `hosts/{hostId}/authors` (AGL-2486) — the custom author\n * this entry publishes under. Absent on every entry written before it, and\n * on any entry whose editor chose a one-off byline instead of a record.\n */\n authorId?: string\n /**\n * The resolved author record, attached by the tenant runtime alongside the\n * `authorName` it denormalizes (`get-collection-content.ts`). Carries the\n * portrait, which is why the byline can show the author's own face rather\n * than the site's mark — see {@link expandCollectionEntryMeta}.\n */\n author?: ContentAuthorRecord | null\n body?: string\n coverImage?: string\n /**\n * The entry's featured video (AGL-2956), stored in the shape\n * {@link coverImage} is: a `media:{scope}/{id}` reference or a URL, a hosted\n * player's media link included (`video-embed-provider.ts`). Raw here for the reason every image field in this\n * file is raw — the resolver needs the rendering host, which this record\n * does not carry.\n *\n * An entry of a video collection is an article whose featured video sits\n * where a post's cover image would, which is what makes its page a watch\n * page. `{{entry.coverVideo}}` binds it into a template's Video element, and\n * the built-in entry page plays it with no template at all.\n */\n coverVideo?: string\n /** Search-result title override (AGL-582); falls back to `title`. */\n seoTitle?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /**\n * Stable reference into the collection's `categories` taxonomy\n * (AGL-582): the ID survives renames, so display names resolve at\n * render via {@link resolveEntryCategoryName}.\n */\n categoryId?: string\n /**\n * Legacy free-typed bucket (AGL-582), e.g. \"Engineering\". Still READ\n * as a fallback everywhere; the editor writes `categoryId` going\n * forward.\n */\n category?: string\n /** Free-form labels (AGL-582), e.g. [\"nextjs\", \"seo\"]. */\n tags?: string[]\n publishedAt?: { seconds: number } | null\n /**\n * The collection this entry came OUT of, stamped where the read happened\n * (AGL-2518).\n *\n * Absent everywhere the routed collection already answers the question,\n * which is every listing the platform had until now: a `/blog` page renders\n * blog entries, so `entry.url` is built from the one slug the route\n * resolved and no entry needs to carry its own.\n *\n * The author page breaks that assumption — it lists a blog post, a\n * changelog note and a press release side by side, and building all three\n * URLs from a single routed slug would produce two links to pages that do\n * not exist. So the reader that mixes collections stamps each entry with\n * where it came from, and {@link collectionEntryTokens} prefers it. Every\n * other caller keeps passing one slug and keeps behaving identically.\n */\n collectionSlug?: string\n /**\n * The display name of {@link collectionSlug} — \"Changelog\", not\n * \"changelog\" — so a card on a mixed listing can label which section a\n * post belongs to. Stamped by the same reader, for the same reason.\n */\n collectionName?: string\n}\n\n/** A collection's published entries, keyed for expansion by its slug. */\nexport interface CollectionEntriesSource {\n slug: string\n entries: CollectionEntryRecord[]\n /** The collection's category taxonomy (AGL-582), for name resolution. */\n categories?: CollectionCategory[]\n /**\n * Whether the READ that produced `entries` came back holding its own\n * `.limit()` (AGL-1516) — a fact recorded where the query ran, because by\n * the time `entries` gets here it has been through a liveness filter and\n * possibly a route's category filter, and its length no longer answers the\n * question. See {@link collectionSourceIsBounded}.\n */\n reachedBound?: boolean\n /**\n * The page the ROUTE asked for (AGL-1321), set only for the collection the\n * URL resolved. Fills in a block that declares `perPage` but no `page` —\n * which is every block a designer can author, since design time cannot\n * know which page a visitor is on. A block that names its own `page` still\n * wins: that is a deliberately pinned window, not a paginated list.\n */\n page?: number\n /**\n * The absolute index `entries[0]` holds in the collection's own order\n * (AGL-3213). Zero everywhere except a listing page past the cached read's\n * bound, which is served by a window read for that page alone.\n *\n * Without it a windowed source is silently empty: every consumer here\n * slices `[(page - 1) * perPage, …)` on the premise that `entries` starts\n * at the beginning of the collection, so page 11 of a windowed ten-entry\n * source slices from 100 and renders nothing.\n */\n windowStart?: number\n}\n\n/**\n * Display name of an entry's category (AGL-582): the `categoryId` lookup\n * into the collection's taxonomy wins; the legacy free-typed\n * `entry.category` string is the fallback so existing data keeps\n * rendering. An ID referencing a deleted category resolves to nothing —\n * by design the entry simply shows no category.\n */\nexport function resolveEntryCategoryName(\n entry: Pick<CollectionEntryRecord, 'categoryId' | 'category'>,\n // `readonly` because this only reads (AGL-1321): the route helpers hold\n // the taxonomy as readonly and would otherwise have to copy it to ask.\n categories?: readonly CollectionCategory[],\n): string | undefined {\n const categoryId = (entry.categoryId ?? '').trim()\n if (categoryId) {\n const match = (categories ?? []).find(\n (category) => category.id === categoryId,\n )\n if (match?.name) return match.name\n }\n const legacy = (entry.category ?? '').trim()\n return legacy || undefined\n}\n\n/* ── Published-date formats (AGL-1459) ──────────────────────────────────── */\n\n\n/**\n * The values an Entry Meta block shows, in the exact spellings the\n * `{{entry.date}}` / `{{entry.author}}` / `{{entry.category}}` /\n * `{{entry.tags}}` tokens produce (AGL-1385, author added AGL-1459).\n *\n * Extracted so the token map and {@link expandCollectionEntryMeta} cannot\n * drift: the same entry has to read the same way whether the author bound the\n * tokens by hand or let the server fill the block in.\n */\nexport function collectionEntryMetaValues(\n entry: CollectionEntryRecord,\n categories?: readonly CollectionCategory[],\n dateFormat?: CollectionEntryDateFormat,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): { date: string; author: string; category: string; tags: string } {\n return {\n date: formatCollectionEntryDate(\n entry.publishedAt,\n dateFormat,\n undefined,\n timeZone,\n ),\n // The per-entry byline the editor already collects (AGL-686). It was\n // reachable in the entry's JSON-LD and nowhere on the page, which is what\n // made the frame's byline unauthorable (AGL-1459).\n //\n // The resolved RECORD first, then the legacy string (AGL-2486) — the\n // precedence `resolveEntryAuthor` applies, spelled the same way here. The\n // tenant denormalizes the record's name onto `authorName` before this\n // runs, so on a live render the two agree; reading the record directly is\n // what makes that a convenience rather than the only thing holding the\n // byline up.\n author: (entry.author?.name || entry.authorName || '').trim(),\n // Entry model v2 (AGL-582): category resolves by stable ID against the\n // collection's taxonomy, falling back to the legacy free-typed string.\n category: resolveEntryCategoryName(entry, categories) ?? '',\n tags: (entry.tags ?? []).join(', '),\n }\n}\n\n/**\n * The values an Entry Author card shows (AGL-2486) — the fields of the\n * resolved author RECORD, in the spellings the `{{entry.author*}}` tokens\n * produce.\n *\n * Extracted for the reason {@link collectionEntryMetaValues} was: the card,\n * the token map and {@link expandCollectionEntryAuthor} have to agree, and\n * three copies of \"name, falling back to the legacy string\" would not.\n *\n * `image` stays RAW — a `media:` reference or a plain url, whichever the\n * author saved — because the resolver that turns one into a fetchable src\n * needs the rendering host and this function has no host (`resolveMediaSrc`,\n * AGL-1215). Every other consumer of an image field in this file does the\n * same.\n */\nexport function collectionEntryAuthorValues(entry: CollectionEntryRecord): {\n name: string\n bio: string\n image: string\n url: string\n pageUrl: string\n links: ContentAuthorLink[]\n} {\n const author = entry.author\n return {\n // Their page on THIS site (AGL-2518/2519) — a different destination from\n // `url`, which is their own. Empty when nothing addresses them, so the\n // byline renders as plain text rather than as a link to `/author/`.\n pageUrl: contentAuthorPageUrl({\n ...(author ? { author } : {}),\n ...(entry.authorId ? { authorId: entry.authorId } : {}),\n ...(entry.authorName ? { authorName: entry.authorName } : {}),\n }),\n // The rows the card PRINTS (AGL-2516). Normalized here rather than\n // trusted, because this is the boundary a stored document crosses to\n // become props: the renderer guards the href too, but only one of the two\n // sits on the path a hand-written document would take.\n links: normalizeContentAuthorLinks(author?.links),\n // The record wins, then the legacy free-typed byline (AGL-686), which is\n // the same precedence `resolveEntryAuthor` applies — an entry written\n // before custom authors still fills a card, with the one field it has.\n name: (author?.name ?? entry.authorName ?? '').trim(),\n bio: (author?.bio ?? '').trim(),\n image: (author?.image ?? '').trim(),\n url: (author?.url ?? '').trim(),\n }\n}\n\n/**\n * The `{{entry.*}}` token map for one entry (AGL-105/551): substituted\n * globally on entry-template screens and per-clone inside the Collection\n * entries block. `entry.url` resolves to the entry's auto-route so links\n * (`Read more`, titles) work without hardcoding the collection slug.\n */\nexport function collectionEntryTokens(\n entry: CollectionEntryRecord,\n collectionSlug: string,\n categories?: CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<string, string> {\n const meta = collectionEntryMetaValues(\n entry,\n categories,\n undefined,\n timeZone,\n )\n const author = collectionEntryAuthorValues(entry)\n // The entry's OWN collection wins over the routed one (AGL-2518) — see\n // `CollectionEntryRecord.collectionSlug`. Unset on every single-collection\n // listing, which is why this changes nothing for them.\n const slug = (entry.collectionSlug ?? '').trim() || collectionSlug\n return {\n 'entry.title': entry.title ?? '',\n 'entry.excerpt': entry.excerpt ?? '',\n 'entry.body': entry.body ?? '',\n 'entry.coverImage': entry.coverImage ?? '',\n // The featured video (AGL-2956), raw like the cover: a template binds it\n // into a Video element's source, and the element resolves a reference\n // against the site rendering it.\n 'entry.coverVideo': entry.coverVideo ?? '',\n 'entry.slug': entry.slug ?? '',\n 'entry.url': `/${slug}/${entry.slug ?? ''}`,\n // Which section this entry belongs to (AGL-2518). Worth binding only on a\n // listing that MIXES collections — the author page — where a card\n // otherwise gives a reader no way to tell a release note from an essay.\n 'entry.collection': (entry.collectionName ?? '').trim(),\n 'entry.collectionSlug': slug,\n 'entry.collectionUrl': slug ? `/${slug}` : '',\n 'entry.date': meta.date,\n // The same instant for a parser (AGL-2956). `entry.date` is written for a\n // reader, so a Video element's publication date bound to it publishes an\n // `uploadDate` no crawler can read; this one is an ISO date-time.\n 'entry.publishedAt': collectionEntryPublishedAtIso(entry.publishedAt),\n // The byline (AGL-1459). Bindable by hand for the same reason every other\n // field is: a template that wants it somewhere Entry Meta does not reach.\n 'entry.author': meta.author,\n // The rest of the author RECORD (AGL-2486). Bindable by hand for the\n // same reason `entry.author` is: a card laid out by the designer rather\n // than dropped as the Entry Author block still has to say who wrote the\n // piece, and typing the blurb in as literal text is how a byline starts\n // naming the wrong person.\n 'entry.authorBio': author.bio,\n 'entry.authorImage': author.image,\n 'entry.authorUrl': author.url,\n /**\n * This author's page on this site (AGL-2518) — \"more from the person who\n * wrote this\".\n *\n * A separate token from `entry.authorUrl`, which is the author's own site,\n * because they are different destinations and a template should be able to\n * offer either or both. Empty when there is nothing addressable, so a\n * binding renders no link rather than one pointing at `/author/`.\n */\n 'entry.authorPageUrl': contentAuthorPageUrl({\n ...(entry.author ? { author: entry.author } : {}),\n ...(entry.authorId ? { authorId: entry.authorId } : {}),\n ...(entry.authorName ? { authorName: entry.authorName } : {}),\n }),\n // Entry model v2 (AGL-582): taxonomy + SEO tokens. The SEO pair falls\n // back to title/excerpt so templates can bind them unconditionally.\n 'entry.category': meta.category,\n 'entry.tags': meta.tags,\n 'entry.seoTitle': entry.seoTitle || entry.title || '',\n 'entry.seoDescription': entry.seoDescription || entry.excerpt || '',\n }\n}\n\n/**\n * Category/tag membership check (AGL-582), shared by the Collection entries\n * filter props and the query-less list surfaces. Matching is trimmed and\n * case-insensitive — editors type these by hand. A category filter matches\n * either the entry's stable `categoryId` or its RESOLVED display name (id\n * lookup first, legacy string fallback), so filters written against either\n * spelling keep working across the taxonomy migration.\n */\nexport function entryMatchesFilter(\n entry: CollectionEntryRecord,\n filter: { category?: string; tag?: string },\n categories?: CollectionCategory[],\n): boolean {\n const normalize = (value: string) => value.trim().toLowerCase()\n if (filter.category) {\n const wanted = normalize(filter.category)\n const matchesId =\n Boolean((entry.categoryId ?? '').trim()) &&\n normalize(entry.categoryId ?? '') === wanted\n const matchesName =\n normalize(resolveEntryCategoryName(entry, categories) ?? '') === wanted\n if (!matchesId && !matchesName) return false\n }\n if (filter.tag) {\n const wanted = normalize(filter.tag)\n if (!(entry.tags ?? []).some((tag) => normalize(tag) === wanted)) {\n return false\n }\n }\n return true\n}\n\n/* ── Category routes (AGL-1321) ─────────────────────────────────────────── */\n\n/**\n * URL form of a category id or display name (AGL-1321): lowercase, runs of\n * non-alphanumerics collapsed to a single `-`, no leading/trailing dash.\n *\n * Deliberately lossy and idempotent, so the same category addresses the same\n * URL whether the author typed the taxonomy id, the display name, or the URL\n * segment itself. `Open source` → `open-source` → `open-source`.\n */\nexport function collectionCategorySlug(value: string | undefined): string {\n return String(value ?? '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/**\n * The taxonomy category a URL segment addresses (AGL-1321), matched on the\n * stable `id` OR the slugified display name — an author linking\n * `/blog/category/open-source` should not have to know which of the two the\n * data happens to store, and an `id` that survives a rename is exactly why\n * the taxonomy has one. `undefined` = the segment names no known category;\n * the caller renders an empty listing rather than crashing.\n */\nexport function resolveCollectionCategoryBySlug(\n categories: readonly CollectionCategory[] | undefined,\n slug: string | undefined,\n): CollectionCategory | undefined {\n const wanted = collectionCategorySlug(slug)\n if (!wanted) return undefined\n return (categories ?? []).find(\n (category) =>\n collectionCategorySlug(category.id) === wanted ||\n collectionCategorySlug(category.name) === wanted,\n )\n}\n\n/**\n * The canonical URL of a collection listing (AGL-1321). ONE builder, because\n * the pills, the pager, the `<link rel=\"canonical\">`, the JSON-LD and the\n * sitemap must all agree on the shape or the filter quietly becomes a\n * duplicate-content generator.\n *\n * The unfiltered page-1 listing is the bare `/{collection}` — there is no\n * `/{collection}/category/all` and no `?category=all`, so \"All\" cannot become\n * a second address for the page that already exists.\n */\nexport function collectionListUrl(options: {\n collectionSlug: string\n categorySlug?: string | null\n page?: number | null\n /** Continue after this entry's document id — the older direction. */\n after?: string | null\n /** Continue before this entry's document id — the newer direction. */\n before?: string | null\n}): string {\n const scoped = options.categorySlug\n ? `/${options.collectionSlug}/${COLLECTION_CATEGORY_ROUTE_SEGMENT}/` +\n collectionCategorySlug(options.categorySlug)\n : `/${options.collectionSlug}`\n\n /*\n * A cursor address (AGL-3219) wins over a page number when both are given,\n * because it is the one that survives a publish at the head of the list.\n */\n const cursor = (options.before || options.after || '').trim()\n if (cursor) {\n const segment = options.before\n ? COLLECTION_BEFORE_ROUTE_SEGMENT\n : COLLECTION_AFTER_ROUTE_SEGMENT\n return `${scoped}/${segment}/${encodeURIComponent(cursor)}`\n }\n\n /*\n * `/{collection}/page/{n}` is still MINTED for a collection that fits\n * inside one read, where it is exactly as stable as it ever was: every one\n * of those pages is sliced out of a single cached snapshot, so no publish\n * can move one page's entries relative to another's. It is past the bound —\n * where the head and the tail were two different reads of a moving list —\n * that a number stopped being an address, and there the pager mints\n * cursors instead.\n */\n const page = Number(options.page)\n return Number.isFinite(page) && page > 1\n ? `${scoped}/${COLLECTION_PAGE_ROUTE_SEGMENT}/${Math.floor(page)}`\n : scoped\n}\n\n/** Where a listing's pager can go from here (AGL-1386). */\nexport interface CollectionPaginationLinks {\n /** 1-based current page; 1 when the listing is unpaginated. */\n page: number\n /**\n * Total pages in the CURRENT set.\n *\n * @deprecated AGL-3219 — 0 when it cannot be known, which is any collection\n * past one read's bound. See `CollectionPagination.totalPages`.\n */\n totalPages: number\n /** The previous page's URL, or `''` on the first page. */\n prevUrl: string\n /** The next page's URL, or `''` when nothing older follows. */\n nextUrl: string\n}\n\n/**\n * Everything a pager needs for one listing (AGL-1386): which page it is on,\n * how many there are, and where prev/next go — through\n * {@link collectionListUrl}, so both links stay inside the category they are\n * paging rather than dumping the reader back onto the unfiltered list.\n *\n * **The edges resolve to the empty string, never to a URL.** No previous page\n * means `prevUrl` is `''`; no next page means `nextUrl` is `''`. That is what\n * lets an authored template bind these unconditionally: one static list screen\n * serves the bare listing, every `/page/{n}`, and every\n * `/category/{slug}` — and there is no runtime conditional to hide a link\n * with. A link whose href resolves to `''` renders as an inert placeholder of\n * the same element (AGL-1268/1357), which is the correct pager on page 1 of 1.\n *\n * ONE computation, because it has two consumers that must agree: the built-in\n * fallback pager's nodes and the `{{pagination.*}}` tokens an authored\n * template binds. A second derivation is how a fallback and a template start\n * disagreeing about which page they are on.\n */\nexport function collectionPaginationLinks(options: {\n collectionSlug: string\n categorySlug?: string | null\n /** 1-based current page; anything unusable reads as page 1. */\n page?: number | null\n /**\n * Total pages.\n *\n * @deprecated AGL-3219 — no longer consulted for whether a next link\n * exists; {@link collectionPaginationLinks} asks `nextCursor` instead.\n */\n totalPages?: number | null\n /**\n * The document id the older page continues from, or `''` when there is no\n * older page (AGL-3219).\n *\n * This is the ONLY thing that decides whether `nextUrl` is a link. It comes\n * from a `limit(perPage + 1)` probe, so an empty string means the read\n * looked for an eleventh entry and there was not one — which is a fact\n * about the collection, unlike a total that two differently-cached reads\n * could disagree about.\n */\n nextCursor?: string | null\n /** The document id the newer page continues before, or `''`. */\n prevCursor?: string | null\n}): CollectionPaginationLinks {\n const positive = (value: number | null | undefined): number => {\n const parsed = Math.floor(Number(value))\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 1\n }\n const page = positive(options.page)\n const totalPagesRaw = Math.floor(Number(options.totalPages))\n /*\n * Zero means UNKNOWN, and only a caller that deals in cursors can mean it\n * (AGL-3219).\n *\n * A listing past the bound passes cursors and no total, and 0 is how it\n * says the number cannot be had. A caller that passes neither — an entry\n * route, which has no listing at all — is the inert case the tokens have\n * always answered with \"page 1 of 1\", and it keeps that answer rather than\n * telling an entry template it is on page 1 of 0.\n */\n const knowsCursors =\n options.nextCursor !== undefined || options.prevCursor !== undefined\n const totalPages =\n Number.isFinite(totalPagesRaw) && totalPagesRaw > 0\n ? totalPagesRaw\n : knowsCursors\n ? 0\n : 1\n const scope = {\n collectionSlug: options.collectionSlug,\n ...(options.categorySlug ? { categorySlug: options.categorySlug } : {}),\n }\n const next = (options.nextCursor ?? '').trim()\n const prev = (options.prevCursor ?? '').trim()\n\n /*\n * Numbered while the whole set fits inside ONE read, cursors once it does\n * not (AGL-3219).\n *\n * A total is only ever stated for a collection the shared read holds\n * entirely, and there every page is sliced out of that one cached snapshot:\n * no publish can move one page's entries relative to another's, so a number\n * is a perfectly good address and stays the one readers and crawlers\n * already have. Past the bound the head and the tail were two reads of a\n * moving list, a number stopped naming the same ten entries from one render\n * to the next, and the pager mints the cursor its own page proves instead.\n */\n const numbered = totalPages > 0\n const numberedHref = (n: number) => collectionListUrl({ ...scope, page: n })\n\n return {\n page,\n totalPages,\n /*\n * Page 2 going back lands on the bare listing either way: it is the head\n * of the collection, and `/{collection}` is its address — there is no\n * cursor for \"the beginning\".\n */\n prevUrl:\n page === 2\n ? collectionListUrl(scope)\n : page > 2\n ? numbered\n ? numberedHref(page - 1)\n : prev\n ? collectionListUrl({ ...scope, before: prev })\n : ''\n : '',\n nextUrl: numbered\n ? page < totalPages\n ? numberedHref(page + 1)\n : ''\n : next\n ? collectionListUrl({ ...scope, after: next })\n : '',\n }\n}\n\n/** A content-collection path resolved to what it addresses (AGL-1321). */\nexport interface CollectionRoute {\n collectionSlug: string\n /** Set only for `/{collection}/{entry}`. */\n entrySlug?: string\n /** Set only for `/{collection}/category/{slug}[/page/{n}]`. */\n categorySlug?: string\n /** 1-based list page; always 1 for entry routes. */\n page: number\n /** Set only for `/{collection}/after/{cursor}` (AGL-3219). */\n after?: string\n /** Set only for `/{collection}/before/{cursor}` (AGL-3219). */\n before?: string\n}\n\nconst POSITIVE_INTEGER = /^[1-9]\\d*$/\n\n/**\n * Parses a non-screen path against the content-collection route shapes\n * (AGL-81/620/1321), or `null` when it is none of them and the caller should\n * fall through to the 404 chain:\n *\n * ```\n * /{collection} list, page 1, all categories\n * /{collection}/{entry} one entry\n * /{collection}/page/{n} list page n\n * /{collection}/category/{slug} filtered list, page 1\n * /{collection}/category/{slug}/page/{n} filtered list, page n\n * ```\n *\n * Pure, and shared by the tenant loader and its tests, because the route\n * table IS the ISR cache key here: the tenant catch-all caches by PATH, so a\n * listing variant that lives in the path gets its own cache entry for free,\n * while one that lived in a query string would share `/blog`'s entry and\n * serve one category's HTML for another.\n */\nexport function parseCollectionRoute(\n segments: readonly string[],\n): CollectionRoute | null {\n const [collectionSlug, ...rest] = segments\n if (!collectionSlug) return null\n if (!rest.length) return { collectionSlug, page: 1 }\n if (rest.length === 1) return { collectionSlug, entrySlug: rest[0], page: 1 }\n if (rest.length === 2 && rest[0] === COLLECTION_PAGE_ROUTE_SEGMENT) {\n return POSITIVE_INTEGER.test(rest[1])\n ? { collectionSlug, page: Number(rest[1]) }\n : null\n }\n /*\n * `/{collection}/after/{cursor}` and its `before` twin (AGL-3219).\n *\n * Checked BEFORE the category shape and after the one-segment entry shape,\n * so an entry whose slug happens to be `after` still answers at\n * `/{collection}/after` — that is one segment, and this is two.\n *\n * `page` stays 1: nothing counts a cursor address, and it is the display\n * label rather than a position anything reads back.\n */\n if (\n rest.length === 2 &&\n (rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT ||\n rest[0] === COLLECTION_BEFORE_ROUTE_SEGMENT) &&\n rest[1]\n ) {\n const cursor = decodeURIComponent(rest[1]).trim()\n if (!cursor) return null\n return rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT\n ? { collectionSlug, after: cursor, page: 1 }\n : { collectionSlug, before: cursor, page: 1 }\n }\n if (rest.length === 2 && rest[0] === COLLECTION_CATEGORY_ROUTE_SEGMENT) {\n const categorySlug = collectionCategorySlug(rest[1])\n return categorySlug ? { collectionSlug, categorySlug, page: 1 } : null\n }\n if (\n rest.length === 4 &&\n rest[0] === COLLECTION_CATEGORY_ROUTE_SEGMENT &&\n rest[2] === COLLECTION_PAGE_ROUTE_SEGMENT &&\n POSITIVE_INTEGER.test(rest[3])\n ) {\n const categorySlug = collectionCategorySlug(rest[1])\n return categorySlug\n ? { collectionSlug, categorySlug, page: Number(rest[3]) }\n : null\n }\n return null\n}\n\n/**\n * Does this entry belong to the category a URL segment addresses (AGL-1321)?\n *\n * Every spelling the route could plausibly mean is slugified into one set and\n * checked against both of the entry's own spellings — the stable `categoryId`\n * and the RESOLVED display name. So `/blog/category/open-source` answers for\n * a taxonomy whose id is `opensource`, for one whose id is opaque but whose\n * name is \"Open source\", and for a legacy free-typed entry that predates the\n * taxonomy entirely. `entryMatchesFilter` cannot stand in: it compares raw\n * trimmed strings, so it would miss every multi-word category name.\n */\nexport function entryMatchesCategoryRoute(\n entry: CollectionEntryRecord,\n route: { slug: string; category?: CollectionCategory },\n categories?: readonly CollectionCategory[],\n): boolean {\n const wanted = new Set(\n [route.slug, route.category?.id, route.category?.name]\n .map((value) => collectionCategorySlug(value))\n .filter(Boolean),\n )\n if (!wanted.size) return false\n const entryId = collectionCategorySlug(entry.categoryId)\n if (entryId && wanted.has(entryId)) return true\n const entryName = collectionCategorySlug(\n resolveEntryCategoryName(entry, categories),\n )\n return Boolean(entryName) && wanted.has(entryName)\n}\n\n/** One category pill as stamped onto a Category Pills block (AGL-1321). */\nexport interface CollectionCategoryLink {\n label: string\n href: string\n /** The pill for the listing currently being rendered. */\n active: boolean\n}\n\n/** Label the unfiltered pill carries when the author never named one. */\nexport const COLLECTION_ALL_PILL_DEFAULT = 'All'\n\n/**\n * The value that persists \"no All pill\" (AGL-1336).\n *\n * The attribute documented \"clear it to omit that pill\" and could not do it:\n * the attributes form strips an emptied text field's KEY (ddf maps an\n * emptied field to its `clearedValue`, final-form's default parse turns\n * `''` into `undefined`, and `updateNodeProps` replaces the props object),\n * so the absent key took {@link COLLECTION_ALL_PILL_DEFAULT} and the pill\n * came straight back. `''` is therefore not a value this field can hold —\n * the same AGL-1191 trap that made the App Bar's \"Default\" unpersistable.\n *\n * So the field carries this as its `clearedValue`: clearing the box writes\n * the word `none`, which survives the round trip and is honoured here.\n * Deliberately a readable word rather than a magic character — an author who\n * reopens the panel sees `none` in the All-label box and can read what it\n * did. Removing the prop entirely (the ✕ affordance) still means \"unset\",\n * and restores the default.\n */\nexport const COLLECTION_ALL_PILL_NONE = 'none'\n\n/**\n * What the unfiltered pill should be labelled, or `''` to omit it — the ONE\n * place the {@link COLLECTION_ALL_PILL_NONE} sentinel is interpreted\n * (AGL-1336). Matched case-insensitively on the trimmed value, because the\n * sentinel is typed by a human only when they are re-entering it by hand.\n *\n * `undefined` is UNSET and takes the default. `null` is CLEARED and omits —\n * it reaches here only from a hand-edited or imported document, but the two\n * are different questions and must not collapse into one answer.\n */\nexport function resolveCollectionAllLabel(value?: string | null): string {\n if (value === undefined) return COLLECTION_ALL_PILL_DEFAULT\n if (value === null) return ''\n const text = `${value}`.trim()\n if (!text) return ''\n return text.toLowerCase() === COLLECTION_ALL_PILL_NONE ? '' : text\n}\n\n/**\n * The pill row for a collection (AGL-1321): \"All\" pointing at the canonical\n * unfiltered listing, then one pill per taxonomy category.\n *\n * Each pill addresses its category by the STABLE id where there is one, so a\n * rename moves the label without breaking the link. Active state is decided\n * through the same resolver the route used, not by string equality, because\n * the URL may legitimately have named the category by either spelling.\n */\nexport function buildCollectionCategoryLinks(options: {\n collectionSlug: string\n categories?: readonly CollectionCategory[]\n activeCategorySlug?: string\n /**\n * Label for the unfiltered pill. Absent takes the default; `''` or the\n * {@link COLLECTION_ALL_PILL_NONE} sentinel omits the pill.\n */\n allLabel?: string | null\n}): CollectionCategoryLink[] {\n const { collectionSlug } = options\n const active = collectionCategorySlug(options.activeCategorySlug)\n const links: CollectionCategoryLink[] = []\n for (const category of options.categories ?? []) {\n const idSlug = collectionCategorySlug(category.id)\n const nameSlug = collectionCategorySlug(category.name)\n const slug = idSlug || nameSlug\n if (!slug) continue\n links.push({\n label: category.name,\n href: collectionListUrl({ collectionSlug, categorySlug: slug }),\n active: Boolean(active) && (idSlug === active || nameSlug === active),\n })\n }\n // A lone \"All\" pill is not a filter, it is decoration — a collection with\n // no taxonomy gets no row at all.\n if (!links.length) return []\n const allLabel = resolveCollectionAllLabel(options.allLabel)\n return allLabel\n ? [\n {\n label: allLabel,\n href: collectionListUrl({ collectionSlug }),\n active: !active,\n },\n ...links,\n ]\n : links\n}\n\n/**\n * The matchable text a search box holds for a set of entries (AGL-1516),\n * built ONCE for both blocks that carry one — the entries block's in-place\n * filter and the standalone toolbar box (AGL-1516, Figma 494:1220).\n *\n * Shared rather than copied for the same reason\n * {@link COLLECTION_SEARCH_FUSE_OPTIONS} is: the two boxes can sit on the\n * same page, over the same collection, and a reader who sees a suggestion in\n * one and a miss in the other has been told the site is broken. Two builders\n * is how the row's date, chip and link drift apart.\n *\n * The row's link, date and category are resolved HERE, where the entry still\n * exists — a component only holds rendered markup by the time it runs, and\n * the suggestion panel draws its rows from this index rather than from the\n * clones. Through the same three resolvers Related posts uses (AGL-1926 for\n * the date especially): a suggestion for a post is the same post as the card\n * below it and must not be able to disagree with it about its date.\n */\nexport function buildCollectionSearchIndex(\n entries: readonly CollectionEntryRecord[],\n collectionSlug: string,\n categories?: readonly CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): CollectionEntrySearchItem[] {\n return entries.map((entry): CollectionEntrySearchItem => {\n const categoryName = resolveEntryCategoryName(entry, categories)\n return {\n title: entry.title ?? '',\n excerpt: entry.excerpt ?? '',\n // Absent keys rather than empty strings, so the row asks one question\n // per field instead of two — the shape Related posts settled on\n // (AGL-1457).\n // The entry's own collection wins over the passed one, as in\n // `collectionEntryTokens` and for the same reason (AGL-2518): a mixed\n // listing's search index must not send every hit to one collection.\n ...(entry.slug\n ? {\n url: `/${(entry.collectionSlug ?? '').trim() || collectionSlug}/${\n entry.slug\n }`,\n }\n : {}),\n ...(entry.publishedAt?.seconds\n ? {\n date: formatCollectionEntryDate(\n entry.publishedAt,\n undefined,\n undefined,\n timeZone,\n ),\n }\n : {}),\n ...(categoryName ? { category: categoryName } : {}),\n }\n })\n}\n\n/**\n * The collection blocks that can be BOUND to a collection by name, rather\n * than inheriting the routed one.\n *\n * The same three `scanCollectionBlocks` resolves a source for, and the same\n * three that can therefore put a collection's entries on a page that is not\n * one of the collection's own routes — a \"Latest posts\" rail on the home\n * page, a category pill strip in a layout's chrome, a search box in a header.\n *\n * Related posts is deliberately absent. It resolves against the routed\n * collection and renders nothing without a current entry in context, so it\n * only ever appears on an entry page, which the entry's own address already\n * covers.\n */\nconst COLLECTION_BOUND_COMPONENT_IDS: readonly string[] = [\n COLLECTION_ENTRIES_COMPONENT_ID,\n COLLECTION_CATEGORIES_COMPONENT_ID,\n COLLECTION_SEARCH_COMPONENT_ID,\n]\n\n/**\n * Does this tree render the collection `collectionSlug` away from the\n * collection's own routes?\n *\n * The content twin of {@link nodesPlaceForm}, and it exists for the same\n * reason: an entry edit changes every page that renders the collection, and\n * those pages are found by SEARCHING node trees rather than by matching a\n * pointer. A collection's own addresses are derivable from its slug; a rail\n * on the home page is not derivable from anything.\n *\n * Matches only an EXPLICIT `collectionSlug`. A block that names none inherits\n * the routed collection — which means it renders nothing at all off-route\n * (`scanCollectionBlocks` adds no slug, so the block is left untouched) and\n * renders the routed collection on-route, where the collection's own list and\n * entry addresses already reach it. Treating an unbound block as a dependent\n * of every collection on the site would make every entry save drop every page\n * that carries one.\n *\n * The comparison is against the slug rather than the document id because that\n * is what the binding stores, and what `scanCollectionBlocks` resolves. A\n * collection whose slug was just changed is a different question, and one the\n * slug change itself has to answer.\n */\nexport function nodesRenderCollection(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n collectionSlug: string,\n): boolean {\n const wanted = String(collectionSlug ?? '').trim()\n if (!wanted) return false\n for (const node of Object.values(nodes ?? {})) {\n if (!node) continue\n if (!COLLECTION_BOUND_COMPONENT_IDS.includes(String(node.componentId))) {\n continue\n }\n const bound = (node.props as Record<string, unknown> | undefined)?.[\n 'collectionSlug'\n ]\n if (typeof bound === 'string' && bound.trim() === wanted) return true\n }\n return false\n}\n\n/**\n * Collection entries blocks (AGL-551): a `collectionEntries` container\n * treats its children as the item template and renders them once per\n * published entry, with `{{entry.*}}` tokens in cloned string props replaced\n * per entry — the content-collection sibling of {@link expandRepeatables}.\n *\n * - The block's collection resolves from its `collectionSlug` prop, falling\n * back to `defaultSlug` (the routed collection on list-template screens).\n * - Clone ids are namespaced `centry__{containerId}__{index}__…` so repeats\n * never collide; run AFTER grafting and BEFORE binding resolution.\n * - Rows are bounded by `props.entriesLimit` and\n * {@link COLLECTION_ENTRIES_MAX}.\n * - `props.firstPageOnly` renders zero rows past page 1 of the ROUTED\n * listing (AGL-1871), so a lead card does not repeat down the pagination.\n * - An UNKNOWN collection leaves the node untouched (fail-open: a renamed\n * collection must never take a screen down). A known collection with no\n * matching entries renders zero rows — never the unsubstituted template.\n * - Inputs are never mutated; template nodes stay in the map unreferenced.\n */\nexport function expandCollectionEntries<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n sourcesBySlug: Record<string, CollectionEntriesSource | undefined>,\n defaultSlug?: string,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n const containers = Object.entries(nodes).filter(\n ([, node]) => node?.componentId === COLLECTION_ENTRIES_COMPONENT_ID,\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const slug =\n String((container.props as any)?.collectionSlug ?? '').trim() ||\n defaultSlug\n const source = slug ? sourcesBySlug[slug] : undefined\n // An UNKNOWN collection leaves the node untouched — fail-open, so a\n // renamed collection never takes a published screen down.\n //\n // A KNOWN collection with nothing to show is a different question, and\n // used to get the same answer (AGL-1321). Leaving the template in place\n // renders it once, with its `{{entry.title}}` / `{{entry.url}}` tokens\n // never substituted, so the page shipped a ghost row of literal braces.\n // That was rare while the only way to empty a list was to publish an\n // empty collection; a category filter makes it reachable from any pill\n // whose category has no posts yet. Zero entries now means zero rows.\n if (!source) continue\n const limitRaw = Number((container.props as any)?.entriesLimit)\n const limit =\n Number.isFinite(limitRaw) && limitRaw > 0\n ? Math.min(limitRaw, COLLECTION_ENTRIES_MAX)\n : COLLECTION_ENTRIES_MAX\n\n // Pagination (AGL-620): when `perPage` is set the block renders one page\n // window `[(page-1)*perPage, page*perPage)`; without it, the legacy\n // `entriesLimit` slice from the top applies unchanged.\n const perPageRaw = Number((container.props as any)?.perPage)\n const perPage =\n Number.isFinite(perPageRaw) && perPageRaw > 0\n ? Math.min(Math.floor(perPageRaw), COLLECTION_ENTRIES_MAX)\n : undefined\n // The block's own `page` wins; otherwise the ROUTE's page applies, so\n // `/blog/page/2` and `/blog/category/guides/page/2` render page 2 of a\n // template listing instead of silently re-serving page 1 at a second URL\n // (AGL-1321). Only ever the routed collection: `source.page` is set\n // nowhere else.\n const pageRaw = Number((container.props as any)?.page)\n const routedPage = Number(source.page)\n const page =\n Number.isFinite(pageRaw) && pageRaw >= 1\n ? Math.floor(pageRaw)\n : Number.isFinite(routedPage) && routedPage >= 1\n ? Math.floor(routedPage)\n : 1\n // \"Only on page 1\" (AGL-1871): a LEAD block takes the top of the set and\n // has no page window of its own, so on `/blog/page/2` it repeats exactly\n // what page 1 led with — same cover, same title, same excerpt — above a\n // page of different posts, under an eyebrow that reads \"Latest\". True on\n // page 1, false everywhere after it.\n //\n // Opt-in and default off. The alternative reading — \"a block with no\n // `perPage` on a paginated route is a lead block\" — is wrong: a \"popular\n // posts\" rail is the same shape and belongs on every page of the listing.\n // Only the author knows which one they built, so the author says so.\n //\n // Gated on the ROUTE's page, never on the block's own pinned `page`. A\n // block that names its own page is a deliberately fixed window (\"the\n // entries page 3 would show, here\"), which says nothing about which page\n // of the listing the reader is on; and off a routed listing entirely\n // (a blog rail on the homepage) `source.page` is unset, so the switch\n // correctly hides nothing.\n const firstPageOnly = Boolean((container.props as any)?.firstPageOnly)\n const suppressedBeyondFirstPage =\n firstPageOnly && Number.isFinite(routedPage) && routedPage > 1\n\n const templateIds = Array.isArray(container.nodes)\n ? (container.nodes as NodeId[])\n : []\n if (!templateIds.length) continue\n\n // Category/tag filter props (AGL-582): the block-level answer to\n // /blog?tag=x — query params never reach the ISR-cached loader, so\n // filtered lists are designed as filtered BLOCKS instead.\n const filterCategory = String(\n (container.props as any)?.filterCategory ?? '',\n ).trim()\n const filterTag = String(\n (container.props as any)?.filterTag ?? '',\n ).trim()\n const filtered =\n filterCategory || filterTag\n ? source.entries.filter((entry) =>\n entryMatchesFilter(\n entry,\n {\n category: filterCategory || undefined,\n tag: filterTag || undefined,\n },\n source.categories,\n ),\n )\n : source.entries\n // Whether the READ behind `source.entries` reached its own bound\n // (AGL-1516). Taken off the raw set, never off `filtered`: a block-level\n // category filter legitimately narrows a complete read, and a narrowed\n // set is not a bounded one. The source's own `reachedBound` wins over\n // counting it, because the count cannot see a read that came back full\n // and was then thinned by the liveness gate.\n const sourceCapped = collectionSourceIsBounded(source)\n\n // Where this source's first entry sits in the collection (AGL-3213):\n // zero for a whole read, and the page's own offset for a windowed one.\n const windowStartRaw = Number(source.windowStart)\n const windowStart =\n Number.isFinite(windowStartRaw) && windowStartRaw > 0\n ? Math.floor(windowStartRaw)\n : 0\n const pageStart = perPage\n ? Math.max(0, (page - 1) * perPage - windowStart)\n : 0\n const windowed = suppressedBeyondFirstPage\n ? []\n : perPage\n ? filtered.slice(pageStart, pageStart + perPage)\n : filtered.slice(0, limit)\n\n const childIds: NodeId[] = []\n windowed.forEach((entry, index) => {\n const prefix = `${COLLECTION_ENTRIES_NODE_ID_PREFIX}${containerId}__${index}__`\n const prefixId = (id: NodeId) => `${prefix}${id}`\n const cloned: Record<NodeId, N> = {}\n const cloneSubtree = (id: NodeId, parentId: NodeId) => {\n const node = nodes[id]\n if (!node) return\n const clonedChildren = Array.isArray(node.nodes)\n ? (node.nodes as NodeId[])\n : undefined\n cloned[prefixId(id)] = {\n ...node,\n $id: prefixId(id),\n parentId,\n ...(clonedChildren && {\n nodes: clonedChildren.map((childId) => prefixId(childId)),\n }),\n }\n clonedChildren?.forEach((childId) =>\n cloneSubtree(childId, prefixId(id)),\n )\n }\n for (const templateId of templateIds) {\n cloneSubtree(templateId, containerId)\n childIds.push(prefixId(templateId))\n }\n // Fill the entry blocks in THIS card from THIS card's entry (AGL-2486).\n //\n // The routed-entry passes (`expandCollectionEntryMeta`,\n // `expandCollectionEntryAuthor`) skip clones deliberately: stamping the\n // routed entry into a listing would date every card the same. That left\n // a cloned block with only its `{{entry.*}}` bindings, and a binding\n // carries no FORMAT — so an Entry Meta inside a card rendered the locale\n // default whatever its Date format said, while the identical block on\n // the article above it obeyed the picker. A field that cannot move the\n // thing it names is worse than a missing one.\n //\n // The fill runs against each clone's own entry, so every card dates\n // itself, and it runs BEFORE substitution so a filled value is already\n // a literal by the time the tokens resolve. The precedence is the\n // shared one: an authored value wins, except the `{{entry.date}}`\n // binding under a chosen format, which is the case this exists for.\n for (const [cloneId, cloneNode] of Object.entries(cloned)) {\n const fill =\n cloneNode?.componentId === COLLECTION_ENTRY_META_COMPONENT_ID\n ? entryMetaFill(cloneNode, entry, source.categories, timeZone)\n : cloneNode?.componentId === COLLECTION_ENTRY_AUTHOR_COMPONENT_ID\n ? entryAuthorFill(cloneNode, entry)\n : undefined\n if (!fill || !Object.keys(fill).length) continue\n cloned[cloneId] = {\n ...cloneNode,\n props: { ...(cloneNode.props ?? {}), ...fill },\n } as N\n }\n Object.assign(\n next,\n resolveNamedTokens(\n cloned,\n collectionEntryTokens(\n entry,\n source.slug,\n source.categories,\n timeZone,\n ),\n ),\n )\n })\n // Search (AGL-1516): the block's search box filters CLIENT-SIDE over the\n // entries this expansion just rendered — the ISR-cached page is the whole\n // data set, so no query ever goes back to Firestore. The component only\n // holds cloned markup by render time, so the matchable text is stamped\n // here, where the entries still exist, index-aligned with the clone\n // groups. Stamped ONLY when the author enabled it: an untouched block's\n // node — and its serialized payload — stays byte-identical.\n const searchEnabled = Boolean((container.props as any)?.search)\n next[containerId] = searchEnabled\n ? {\n ...container,\n props: {\n ...(container.props as any),\n searchIndex: buildCollectionSearchIndex(\n windowed,\n source.slug,\n source.categories,\n timeZone,\n ),\n // How many entries the window above was drawn FROM (AGL-1516).\n //\n // The component has to tell the reader whether a miss means \"not\n // here\" or \"not anywhere\", and it cannot work that out for\n // itself: it sees `perPage`, but `perPage` is only one of three\n // ways this block gets truncated. `entriesLimit` slices too, and\n // so does COLLECTION_ENTRIES_MAX at 100. A block set to \"latest 6\n // posts\" indexes 6 of 40 and, keyed off `perPage` alone, told the\n // reader flatly that their post does not exist.\n //\n // Stamped as the COUNT rather than as a `truncated` verdict, so\n // the component decides the wording and this stays a fact. It is\n // the post-filter total — a category-filtered block searches its\n // category, and \"the rest of the collection\" means the rest of\n // what this block would otherwise show.\n searchTotal: filtered.length,\n // And whether `searchTotal` is itself a ceiling (AGL-1516).\n //\n // `source.entries` is a READ, and that read is bounded — the\n // tenant fetches a collection's live entries with a hard\n // `.limit(100)` and no `orderBy`. On a collection past that bound\n // `filtered.length` reports the bound, not the collection, so\n // `searchIndex.length === searchTotal` — the test the component\n // reads as \"this block holds the whole set\" — is satisfied by a\n // block that holds 100 of 400 posts. The completeness claim is\n // the one thing a truncated read must never be allowed to make:\n // \"no matches\" over a quarter of a collection is a confident lie\n // about the other three quarters.\n //\n // A fact, like the count beside it: \"the read that produced this\n // index reached its bound\". A collection of exactly 100 sets it\n // too, and is described one notch more cautiously than it needed\n // to be — which is the safe direction, and the only one available\n // without a second count query.\n ...(sourceCapped ? { searchCapped: true } : {}),\n },\n nodes: childIds,\n }\n : { ...container, nodes: childIds }\n }\n return next\n}\n\n/**\n * Category Pills blocks (AGL-1321): stamps each `collectionCategories` node\n * with its collection's pill row as a serializable `items` prop the component\n * renders as real anchors — the same server-stamped shape Related posts uses,\n * and for the same reason: the block owns its markup, so there is no template\n * to clone.\n *\n * `activeCategorySlug` marks a pill only on the ROUTED collection. A pills\n * block pointing at some other collection is not what this URL filters, so\n * marking one of its pills current would be a lie in the HTML.\n *\n * A collection with no taxonomy leaves its block untouched, so the component's\n * besigner affordance / empty site render applies. Inputs are never mutated.\n */\nexport function expandCollectionCategories<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n sourcesBySlug: Record<string, CollectionEntriesSource | undefined>,\n defaultSlug?: string,\n activeCategorySlug?: string,\n): Record<NodeId, N> {\n const containers = Object.entries(nodes).filter(\n ([, node]) => node?.componentId === COLLECTION_CATEGORIES_COMPONENT_ID,\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const slug =\n String((container.props as any)?.collectionSlug ?? '').trim() ||\n defaultSlug\n const source = slug ? sourcesBySlug[slug] : undefined\n if (!source) continue\n const allLabel = (container.props as any)?.allLabel\n const items = buildCollectionCategoryLinks({\n collectionSlug: source.slug,\n categories: source.categories,\n ...(slug === defaultSlug ? { activeCategorySlug } : {}),\n // Absent means \"never set\" and takes the default; `null` is a cleared\n // value and is forwarded AS null, because `String(null)` would have\n // labelled the pill \"null\" (AGL-1336).\n ...(allLabel === undefined\n ? {}\n : { allLabel: allLabel === null ? null : String(allLabel) }),\n })\n if (!items.length) continue\n next[containerId] = {\n ...container,\n props: { ...(container.props as any), items },\n }\n }\n return next\n}\n\n/**\n * Collection Search blocks (AGL-1516): stamps each `collectionSearch` node\n * with its collection's search index, the size of the set that index was\n * drawn from, and whether that set was a bounded read — the same\n * server-stamped shape Category Pills and Related posts use, and for the same\n * reason: the block owns its markup, so there is no template to clone.\n *\n * Indexes the WHOLE source rather than a page window. The entries block's box\n * searches the entries that block rendered because it filters them in place;\n * a toolbar box filters nothing and suggests, so scoping it to one page of a\n * listing would be an arbitrary cut nobody asked for. What it can honestly\n * cover is the set the page's listing is drawn from — which on a category\n * route is that category, because `source.entries` arrives filtered.\n *\n * Clones are SKIPPED by their `centry__` prefix: a search box inside an entry\n * template would otherwise be stamped once per card. Nodes are never mutated,\n * and a collection with no entries leaves its block untouched, so the\n * component's besigner affordance and its empty site render both stand.\n */\nexport function expandCollectionSearch<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n sourcesBySlug: Record<string, CollectionEntriesSource | undefined>,\n defaultSlug?: string,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n const containers = Object.entries(nodes).filter(\n ([id, node]) =>\n node?.componentId === COLLECTION_SEARCH_COMPONENT_ID &&\n !id.startsWith(COLLECTION_ENTRIES_NODE_ID_PREFIX),\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const slug =\n String((container.props as any)?.collectionSlug ?? '').trim() ||\n defaultSlug\n const source = slug ? sourcesBySlug[slug] : undefined\n // An unknown collection leaves the node untouched — fail-open, exactly as\n // the entries and pills blocks do, so a rename never takes a screen down.\n if (!source) continue\n // A collection with nothing published gets no box. A search field over an\n // empty index can only ever answer \"no matches\", which reads as a fact\n // about the reader's query rather than about the empty collection it\n // really describes.\n if (!source.entries.length) continue\n next[containerId] = {\n ...container,\n props: {\n ...(container.props as any),\n searchIndex: buildCollectionSearchIndex(\n source.entries,\n source.slug,\n source.categories,\n timeZone,\n ),\n searchTotal: source.entries.length,\n ...(collectionSourceIsBounded(source) ? { searchCapped: true } : {}),\n },\n }\n }\n return next\n}\n\n/**\n * Entry Meta blocks (AGL-1385): stamps each `collectionEntryMeta` node with\n * the routed entry's date, category and tags — the same server-stamped shape\n * Category Pills and Related posts already use.\n *\n * MEASURED on the live marketing blog, which is what this fixes. The block was\n * on `blogEntryTmpl` carrying props `{showDate: true, showCategory: true,\n * showTags: true}` and NOTHING else, and rendered as an empty `<div>` at height\n * 0 — while the very same entry's `<pubDate>` and `Guides` category appeared in\n * its feed item, its JSON-LD, and a heading two nodes above it. So the data was\n * never missing; the block simply had nothing bound to it.\n *\n * The design is what made that reachable. Every other collection block sources\n * itself; this one required the author to type `{{entry.date}}` into a text\n * field. Only the PRESET seeds those tokens — drag the bare component out of\n * the palette and you get three switches labelled \"Show date / Show category /\n * Show tags\", all defaulting to on, gating values that can never exist. That is\n * an affordance promising something the component cannot deliver, so this is a\n * defect in the block and not in anyone's content, and it hits every collection\n * that uses it, not just blog.\n *\n * Deliberately narrow:\n *\n * - An AUTHORED value always wins. Only an absent or blank prop is filled, so\n * a hand-typed byline, a pinned string, or a `{{entry.*}}` token still\n * awaiting substitution is never overwritten. (Blank counts as unset\n * because a cleared text field cannot persist `''` anyway — AGL-1336. The\n * way to hide a value is the switch that exists for it.)\n * - Nodes cloned per entry by {@link expandCollectionEntries} are SKIPPED by\n * their `centry__` id prefix. Their tokens resolve per clone; stamping the\n * routed entry into them would date every card in a listing the same.\n * - Without a routed entry — a list route, a plain screen — nothing is\n * stamped, so the besigner affordance and the empty site render stand.\n *\n * `dateFormat` (AGL-1459) is answered here, where the timestamp still exists,\n * and is the ONE exception to \"an authored value always wins\" — narrowly, and\n * only for the literal `{{entry.date}}` binding. That binding is what the\n * block's own preset seeds, so a format that filled blanks only would do\n * nothing on the exact surface it was built for. The token is not a hand-typed\n * override; it NAMES the value being reformatted. A literal string an author\n * typed still wins, unchanged.\n *\n * Inputs are never mutated.\n */\nexport function expandCollectionEntryMeta<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n entry: CollectionEntryRecord | null | undefined,\n categories?: readonly CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n if (!entry) return nodes\n const containers = Object.entries(nodes).filter(\n ([id, node]) =>\n node?.componentId === COLLECTION_ENTRY_META_COMPONENT_ID &&\n !id.startsWith(COLLECTION_ENTRIES_NODE_ID_PREFIX),\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const filled = entryMetaFill(container, entry, categories, timeZone)\n if (!Object.keys(filled).length) continue\n next[containerId] = {\n ...container,\n props: { ...(container.props ?? {}), ...filled } as any,\n }\n }\n return next\n}\n\n/**\n * The values ONE Entry Meta node takes from ONE entry (AGL-2486).\n *\n * Extracted so a node inside a listing's card can be filled from the entry\n * that card is for, which is a different entry per clone and therefore\n * cannot come from the routed-entry pass above. Both callers apply the same\n * precedence, because the same block cannot mean two things depending on\n * where an author dropped it.\n *\n * Returns only the keys that change; an empty object means \"leave this node\n * exactly as authored\".\n */\nfunction entryMetaFill(\n container: { props?: unknown },\n entry: CollectionEntryRecord,\n categories?: readonly CollectionCategory[],\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<string, string> {\n {\n const props = (container.props ?? {}) as Record<string, unknown>\n // Per node: the format is a prop, so two blocks on one template can read\n // their dates differently.\n const dateFormat = normalizeCollectionEntryDateFormat(props['dateFormat'])\n const values = collectionEntryMetaValues(\n entry,\n categories,\n dateFormat,\n timeZone,\n )\n const filled: Record<string, string> = {}\n for (const key of ['date', 'author', 'category', 'tags'] as const) {\n const authored = String(props[key] ?? '').trim()\n if (authored && !reformattableEntryDate(key, authored, dateFormat)) {\n continue\n }\n if (!values[key]) continue\n filled[key] = values[key]\n }\n // The byline avatar follows the BYLINE, and is the second narrow\n // exception to \"an authored value always wins\" (after `dateFormat`).\n //\n // `avatarImage` was introduced as a block-level pick because entries then\n // carried a byline STRING and no portrait anywhere, so the site's brand\n // mark chosen once on the template was the only answer available. Custom\n // authors changed that: an entry resolves to a record, and a record has\n // an `image`. Keeping the authored mark in front of it would print the\n // company logo beside a named person — the wrong face, on every post that\n // person wrote, which is worse than the generic mark it was standing in\n // for.\n //\n // Deliberately narrow, so nothing else moves: only a resolved author\n // RECORD that actually carries an image displaces the authored value. A\n // legacy string byline, an author with no portrait, and an entry with no\n // author at all all leave the template's mark exactly where it was.\n //\n // And only where a byline is actually PRINTED. This block is also the\n // product's tag row — the article frame ends on one with `Show author`,\n // `Show date` and `Show category` off and nothing but chips left — so a\n // portrait filled in unconditionally lands beside the tags, halfway down\n // the page from the byline it belongs to. The avatar is `alt=\"\"` on the\n // grounds that \"the byline names the author in text right beside it\",\n // which is exactly the thing a chips-only block does not have.\n const authorImage = String(entry.author?.image ?? '').trim()\n const bylineShown =\n props['showAuthor'] !== false &&\n Boolean(filled['author'] || String(props['author'] ?? '').trim())\n if (authorImage && bylineShown && props['showAvatar'] !== false) {\n filled['avatarImage'] = authorImage\n }\n /*\n Where the byline GOES (AGL-2519).\n\n Filled on the same terms as the avatar and for the same reason: it\n belongs to the byline, so it is only filled where a byline is actually\n printed. This block doubles as the product's tag row — the article frame\n ends on one with the author, date and category all switched off — and a\n link stamped there would have nothing to attach to.\n\n An authored value still wins, which is what `Link author` off leaves\n behind once the switch has written an explicit `false`: the switch is\n read by the renderer, and this only supplies the destination.\n */\n if (bylineShown) {\n const pageUrl = collectionEntryAuthorValues(entry).pageUrl\n if (pageUrl && !String(props['authorPageUrl'] ?? '').trim()) {\n filled['authorPageUrl'] = pageUrl\n }\n }\n return filled\n }\n}\n\n/**\n * Fill Entry Author cards from the routed entry's author record (AGL-2486),\n * on the same terms {@link expandCollectionEntryMeta} fills the byline:\n *\n * - an authored value always wins, and a blank one is a blank, not a\n * request for the default — the way to hide a field is its Show switch;\n * - per-entry clones (`centry__`) are skipped, so a listing's cards keep\n * resolving their own tokens rather than every card naming the routed\n * entry's author;\n * - without a routed entry nothing is stamped, which leaves the besigner's\n * placeholder card visible on the canvas.\n *\n * Inputs are never mutated.\n */\nexport function expandCollectionEntryAuthor<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n entry: CollectionEntryRecord | null | undefined,\n): Record<NodeId, N> {\n if (!entry) return nodes\n const containers = Object.entries(nodes).filter(\n ([id, node]) =>\n node?.componentId === COLLECTION_ENTRY_AUTHOR_COMPONENT_ID &&\n !id.startsWith(COLLECTION_ENTRIES_NODE_ID_PREFIX),\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const filled = entryAuthorFill(container, entry)\n if (!Object.keys(filled).length) continue\n next[containerId] = {\n ...container,\n props: { ...(container.props ?? {}), ...filled } as any,\n }\n }\n return next\n}\n\n/**\n * The values ONE Entry Author node takes from ONE entry (AGL-2486) — the\n * companion to {@link entryMetaFill}, extracted for the same reason: a card\n * inside a listing is filled from the entry that card is for.\n */\nfunction entryAuthorFill(\n container: { props?: unknown },\n entry: CollectionEntryRecord,\n): Record<string, unknown> {\n const values = collectionEntryAuthorValues(entry)\n const props = (container.props ?? {}) as Record<string, unknown>\n const filled: Record<string, unknown> = {}\n for (const key of ['name', 'bio', 'image', 'url', 'pageUrl'] as const) {\n if (String(props[key] ?? '').trim()) continue\n if (!values[key]) continue\n filled[key] = values[key]\n }\n // `links` is not authorable as text — a row carries a platform or a picked\n // icon, and the console's author editor is where those are chosen — so\n // there is no authored value to defer to. The record's rows are the only\n // ones there are; `Show links` is the template's control over them.\n if (values.links.length) filled['links'] = values.links\n return filled\n}\n\n/** The one binding a chosen date format may replace (AGL-1459). */\nconst ENTRY_DATE_BINDING = '{{entry.date}}'\n\n/**\n * Is this authored value the `{{entry.date}}` binding under a chosen format\n * (AGL-1459)? Anything else — a literal string, another token, the default\n * format — answers no, and the authored value stands.\n */\nfunction reformattableEntryDate(\n key: string,\n authored: string,\n dateFormat: CollectionEntryDateFormat,\n): boolean {\n return (\n key === 'date' &&\n authored === ENTRY_DATE_BINDING &&\n dateFormat !== COLLECTION_ENTRY_DATE_FORMAT_DEFAULT\n )\n}\n\n/**\n * The ONE matcher configuration behind every entry search in the product\n * (AGL-1516/AGL-1525) — the block's toolbar box, its suggestion panel, and\n * the site-wide results page the panel links to.\n *\n * Shared rather than copied because the two halves are joined by a link the\n * reader clicks: the panel's \"View all results\" hands its query to /search,\n * and if the results page matched more strictly it would answer a visible\n * suggestion with \"no results\". A typo the panel forgave — \"platfrom\" — is\n * exactly the query most likely to make that trip.\n *\n * The shape is the icon picker's (use-mdi-icons-fuzzy), tuned once for prose\n * and MEASURED before shipping: the title names the post and the excerpt\n * merely describes it, so they are weighted 0.7/0.3; the default location\n * scoring buries a legitimate mid-sentence hit in a sentence-long excerpt,\n * and the default 0.6 threshold matches \"media\" against every post on letter\n * soup, so location is ignored and the threshold tightened to 0.3.\n *\n * A plain object, not a Fuse instance: `libs/aglyn` is dependency-light by\n * design, and each consumer already imports Fuse from the vendor bundle.\n */\nexport const COLLECTION_SEARCH_FUSE_OPTIONS = {\n keys: [\n { name: 'title', weight: 0.7 },\n { name: 'excerpt', weight: 0.3 },\n ],\n includeScore: true,\n shouldSort: true,\n ignoreLocation: true,\n threshold: 0.3,\n}\n\n/**\n * One entry of a Collection Entries search index (AGL-1516): the fields the\n * block's client-side search box matches against, index-aligned with the\n * entry clones the block rendered. Stamped by {@link expandCollectionEntries}\n * only when the block's `search` prop is on; never set by hand.\n */\nexport interface CollectionEntrySearchItem {\n title: string\n excerpt: string\n /**\n * Entry permalink (AGL-1525). The `filter` mode never needs it — it hides\n * and shows clones that already carry their own links — but a suggestion\n * row IS the link, and the panel is drawn from the index, not from the\n * clones. Optional because a page cached before this shipped has an index\n * without it; the panel degrades to plain text rather than to a dead link.\n */\n url?: string\n /** Published date, through {@link formatCollectionEntryDate} (AGL-1525). */\n date?: string\n /** Category display name for the suggestion row's chip (AGL-1525). */\n category?: string\n}\n\n/** One related post as stamped onto a Related posts block (AGL-582). */\nexport interface CollectionRelatedItem {\n title: string\n url: string\n date?: string\n excerpt?: string\n /** Per-entry byline (AGL-686); falls back to the site as author. */\n authorName?: string\n category?: string\n /**\n * The entry's cover, as STORED (AGL-1457) — a `media:` reference, a legacy\n * URL, or a hotlink. Left unresolved on purpose: the block resolves it at\n * render through `resolveMediaSrc`, exactly as the Image element does, so\n * one reference keeps working across sites and CDN route changes.\n */\n coverImage?: string\n /**\n * The author's description of that cover (AGL-2418). Travels WITH\n * `coverImage`, and absent means \"fall back to the title\" rather than\n * \"silent\" — on this block the cover is the link's own content.\n */\n coverImageAlt?: string\n}\n\n/**\n * Related-post selection (AGL-582): other entries of the same collection\n * that share the current entry's category or at least one tag, newest\n * first. Pure so the compose stage and tests share one implementation. An\n * entry with no category and no tags relates to nothing — the caller\n * renders nothing rather than guessing.\n *\n * Two entries share a category when their stable `categoryId`s are equal\n * (survives renames AND deletions) or their RESOLVED display names match\n * case-insensitively — so a legacy free-typed entry still relates to a\n * migrated `categoryId` entry whose taxonomy name spells the same.\n */\nexport function selectRelatedEntries(\n entries: CollectionEntryRecord[],\n current: CollectionEntryRecord,\n limit = COLLECTION_RELATED_DEFAULT_LIMIT,\n categories?: CollectionCategory[],\n): CollectionEntryRecord[] {\n const categoryId = (current.categoryId ?? '').trim()\n const categoryName = (\n resolveEntryCategoryName(current, categories) ?? ''\n ).toLowerCase()\n const tags = (current.tags ?? []).map((tag) => tag.trim()).filter(Boolean)\n if (!categoryId && !categoryName && !tags.length) return []\n const bounded = Math.min(\n Math.max(limit, 0) || COLLECTION_RELATED_DEFAULT_LIMIT,\n COLLECTION_RELATED_MAX,\n )\n return entries\n .filter((entry) => {\n if (\n (entry.$id && entry.$id === current.$id) ||\n (entry.slug && entry.slug === current.slug)\n ) {\n return false\n }\n if (categoryId && (entry.categoryId ?? '').trim() === categoryId) {\n return true\n }\n if (\n categoryName &&\n (resolveEntryCategoryName(entry, categories) ?? '')\n .trim()\n .toLowerCase() === categoryName\n ) {\n return true\n }\n return tags.some((tag) => entryMatchesFilter(entry, { tag }))\n })\n .sort(\n (a, b) => (b.publishedAt?.seconds ?? 0) - (a.publishedAt?.seconds ?? 0),\n )\n .slice(0, bounded)\n}\n\n/**\n * Related posts blocks (AGL-582): stamps each `collectionRelated` node with\n * the current entry's related posts as a serializable `entries` prop the\n * component renders directly (no template cloning — the block owns its\n * markup). Runs only on entry renders; without an entry context the nodes\n * stay untouched, so the component's sample cards (editing surfaces) or its\n * empty site render apply. Inputs are never mutated.\n */\nexport function expandCollectionRelated<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n nodes: Record<NodeId, N>,\n source: CollectionEntriesSource | undefined,\n currentEntry: CollectionEntryRecord | null | undefined,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Record<NodeId, N> {\n if (!source || !currentEntry) return nodes\n const containers = Object.entries(nodes).filter(\n ([, node]) => node?.componentId === COLLECTION_RELATED_COMPONENT_ID,\n )\n if (!containers.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [containerId, container] of containers) {\n const limitRaw = Number((container.props as any)?.limit)\n // Per node, exactly as the byline reads it (AGL-1459): the format is a\n // prop, so two rails on one template can date their cards differently.\n const dateFormat = normalizeCollectionEntryDateFormat(\n (container.props as any)?.dateFormat,\n )\n const related = selectRelatedEntries(\n source.entries ?? [],\n currentEntry,\n Number.isFinite(limitRaw) && limitRaw > 0\n ? limitRaw\n : COLLECTION_RELATED_DEFAULT_LIMIT,\n source.categories,\n )\n const items: CollectionRelatedItem[] = related.map((entry) => {\n const categoryName = resolveEntryCategoryName(entry, source.categories)\n return {\n title: entry.title ?? '',\n url: `/${source.slug}/${entry.slug ?? ''}`,\n // Through the one formatter (AGL-1926), not a second inline\n // `toLocaleDateString()`. It produced the same bytes only because\n // this runs on the server, where the implicit locale and zone happen\n // to be the ones `formatCollectionEntryDate` now pins; a related-post\n // card is the same published date as the byline above it and must not\n // be able to disagree with it.\n //\n // Answered HERE and not in the block (AGL-2486), for the reason the\n // byline's format is: by the time a date reaches the component it is\n // a formatted string, and re-parsing one is ambiguous by\n // construction — `8/9/2026` is 9 August under `en-US` and 8 September\n // under `en-GB`. The timestamp only exists on this side.\n ...(entry.publishedAt?.seconds\n ? {\n date: formatCollectionEntryDate(\n entry.publishedAt,\n dateFormat,\n undefined,\n timeZone,\n ),\n }\n : {}),\n ...(entry.excerpt ? { excerpt: entry.excerpt } : {}),\n // AGL-1457: the block owns its markup, so there is no template to\n // bind `{{entry.coverImage}}` on — the cover can only reach it as a\n // stamped field. Absent covers omit the key rather than stamping an\n // empty string, so the render asks one question, not two.\n ...(entry.coverImage ? { coverImage: entry.coverImage } : {}),\n // Only alongside a cover, and only when authored (AGL-2418) — a\n // description with no picture is dead weight on every stamped node.\n ...(entry.coverImage && (entry as any).coverImageAlt\n ? { coverImageAlt: (entry as any).coverImageAlt }\n : {}),\n ...(categoryName ? { category: categoryName } : {}),\n }\n })\n next[containerId] = {\n ...container,\n props: { ...(container.props as any), entries: items },\n }\n }\n return next\n}\n\nexport default expandCollectionEntries\n"],"names":["COLLECTION_ENTRY_DATE_FORMAT_DEFAULT","collectionEntryPublishedAtIso","formatCollectionEntryDate","normalizeCollectionEntryDateFormat","contentAuthorPageUrl","normalizeContentAuthorLinks","resolveNamedTokens","COLLECTION_ENTRIES_COMPONENT_ID","COLLECTION_ENTRY_BODY_COMPONENT_ID","COLLECTION_RELATED_COMPONENT_ID","COLLECTION_SHARE_COMPONENT_ID","COLLECTION_ENTRY_META_COMPONENT_ID","COLLECTION_ENTRY_AUTHOR_COMPONENT_ID","COLLECTION_CATEGORIES_COMPONENT_ID","COLLECTION_SEARCH_COMPONENT_ID","COLLECTION_PAGE_ROUTE_SEGMENT","COLLECTION_CATEGORY_ROUTE_SEGMENT","COLLECTION_AFTER_ROUTE_SEGMENT","COLLECTION_BEFORE_ROUTE_SEGMENT","COLLECTION_ENTRIES_NODE_ID_PREFIX","COLLECTION_ENTRIES_MAX","COLLECTION_SOURCE_MAX","collectionSourceReachedBound","entries","length","collectionSourceIsBounded","source","reachedBound","COLLECTION_LIST_PAGE_SIZE","collectionTotalPages","total","perPage","Math","max","ceil","collectionEntriesPageWindow","pagination","Number","isFinite","pageRaw","page","floor","startRaw","windowStart","start","slice","COLLECTION_RELATED_DEFAULT_LIMIT","COLLECTION_RELATED_MAX","COLLECTION_CATEGORIES_MAX","ENTRIES_MAX_PER_COLLECTION","COLLECTIONS_MAX_PER_HOST","resolveEntryCategoryName","entry","categories","categoryId","trim","match","find","category","id","name","legacy","undefined","collectionEntryMetaValues","dateFormat","timeZone","date","publishedAt","author","authorName","tags","join","collectionEntryAuthorValues","pageUrl","authorId","links","bio","image","url","collectionEntryTokens","collectionSlug","meta","slug","title","excerpt","body","coverImage","coverVideo","collectionName","seoTitle","seoDescription","entryMatchesFilter","filter","normalize","value","toLowerCase","wanted","matchesId","Boolean","matchesName","tag","some","collectionCategorySlug","String","replace","resolveCollectionCategoryBySlug","collectionListUrl","options","scoped","categorySlug","cursor","before","after","segment","encodeURIComponent","collectionPaginationLinks","positive","parsed","totalPagesRaw","totalPages","knowsCursors","nextCursor","prevCursor","scope","next","prev","numbered","numberedHref","n","prevUrl","nextUrl","POSITIVE_INTEGER","parseCollectionRoute","segments","rest","entrySlug","test","decodeURIComponent","entryMatchesCategoryRoute","route","Set","map","size","entryId","has","entryName","COLLECTION_ALL_PILL_DEFAULT","COLLECTION_ALL_PILL_NONE","resolveCollectionAllLabel","text","buildCollectionCategoryLinks","active","activeCategorySlug","idSlug","nameSlug","push","label","href","allLabel","buildCollectionSearchIndex","categoryName","seconds","COLLECTION_BOUND_COMPONENT_IDS","nodesRenderCollection","nodes","node","Object","values","includes","componentId","bound","props","expandCollectionEntries","sourcesBySlug","defaultSlug","containers","containerId","container","limitRaw","entriesLimit","limit","min","perPageRaw","routedPage","firstPageOnly","suppressedBeyondFirstPage","templateIds","Array","isArray","filterCategory","filterTag","filtered","sourceCapped","windowStartRaw","pageStart","windowed","childIds","forEach","index","prefix","prefixId","cloned","cloneSubtree","parentId","clonedChildren","$id","childId","templateId","cloneId","cloneNode","fill","entryMetaFill","entryAuthorFill","keys","assign","searchEnabled","search","searchIndex","searchTotal","searchCapped","expandCollectionCategories","items","expandCollectionSearch","startsWith","expandCollectionEntryMeta","filled","key","authored","reformattableEntryDate","authorImage","bylineShown","expandCollectionEntryAuthor","ENTRY_DATE_BINDING","COLLECTION_SEARCH_FUSE_OPTIONS","weight","includeScore","shouldSort","ignoreLocation","threshold","selectRelatedEntries","current","bounded","sort","a","b","expandCollectionRelated","currentEntry","related","coverImageAlt"],"mappings":"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"}