@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
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { buildPageMarkdown, headingLevelOf, isPageChromeNode, pageContentRootId } from "./page-markdown.js";
17
+ /**
18
+ * What a page SAYS, read off its node tree (AGL-2910): the text a search
19
+ * listing summarizes, the headings, and the images — each heading and image
20
+ * by node id, so a finding can name the element it is about and a fix can
21
+ * address the element it changes.
22
+ *
23
+ * Pure, and the platform's: the SEO check (`seo-audit.ts`) judges a page by
24
+ * these facts, and anything that proposes a listing or a content fix reads
25
+ * the same ones rather than walking the tree its own way.
26
+ *
27
+ * The text is the page's Markdown representation, the one an agent is
28
+ * served, so what the listing summarizes is what a reader of the page gets.
29
+ * The walk starts where that serializer starts — the content region — and
30
+ * skips what it skips, through the same chrome predicate: a heading in the
31
+ * site's navigation is not the page's heading.
32
+ */ /** How much of a page's text the facts carry. A listing summarizes; it does not need the page. */ export const SEO_PAGE_TEXT_MAX_CHARS = 2000;
33
+ /** How much text around an image describes it. */ const IMAGE_CONTEXT_MAX_CHARS = 160;
34
+ /** The deepest a walk goes, and it stops at a cycle — a node map is data. */ const MAX_DEPTH = 64;
35
+ /** The text components whose heading level `headingLevelOf` reads. */ const TEXT_COMPONENTS = new Set([
36
+ 'muiTypography',
37
+ 'muiInlineText',
38
+ 'muiListItemText'
39
+ ]);
40
+ const collapse = (value)=>value.replace(/\s+/g, ' ').trim();
41
+ /** Author HTML to its text, for a heading stored as rich text. */ function htmlText(html) {
42
+ return collapse(html.replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&'));
43
+ }
44
+ /** A text component's own text, and whether it is plain (a fix can rewrite it). */ function textOf(props) {
45
+ const html = props['html'];
46
+ if (typeof html === 'string' && html.trim()) return {
47
+ text: htmlText(html),
48
+ plain: false
49
+ };
50
+ const children = props['children'];
51
+ return typeof children === 'string' ? {
52
+ text: collapse(children),
53
+ plain: true
54
+ } : {
55
+ text: '',
56
+ plain: true
57
+ };
58
+ }
59
+ const childIdsOf = (node)=>Array.isArray(node == null ? void 0 : node.nodes) ? (node == null ? void 0 : node.nodes).filter((id)=>typeof id === 'string') : [];
60
+ /** Read the facts a listing and the SEO check need from one page's node map. */ export function seoPageFacts(nodes, options = {}) {
61
+ var _options_maxChars;
62
+ const empty = {
63
+ text: '',
64
+ wordCount: 0,
65
+ headings: [],
66
+ h1s: [],
67
+ images: [],
68
+ imagesMissingAlt: [],
69
+ contentRootId: null
70
+ };
71
+ if (!nodes) return empty;
72
+ const contentRootId = pageContentRootId(nodes, options.rootId);
73
+ if (!contentRootId) return empty;
74
+ const markdown = buildPageMarkdown({
75
+ nodes,
76
+ rootId: options.rootId
77
+ });
78
+ const text = markdown.trim().slice(0, (_options_maxChars = options.maxChars) != null ? _options_maxChars : SEO_PAGE_TEXT_MAX_CHARS);
79
+ const headings = [];
80
+ const images = [];
81
+ let lastText = '';
82
+ const seen = new Set();
83
+ const visit = (id, depth)=>{
84
+ var _node_props, _node_componentId, _props_html;
85
+ if (depth > MAX_DEPTH || seen.has(id)) return;
86
+ const node = nodes[id];
87
+ if (!node) return;
88
+ seen.add(id);
89
+ if (isPageChromeNode(node)) return;
90
+ const props = (_node_props = node.props) != null ? _node_props : {};
91
+ const componentId = String((_node_componentId = node.componentId) != null ? _node_componentId : '');
92
+ if (TEXT_COMPONENTS.has(componentId)) {
93
+ const { text: own, plain } = textOf(props);
94
+ const level = headingLevelOf(props);
95
+ if (own) {
96
+ if (level > 0) headings.push({
97
+ nodeId: id,
98
+ level,
99
+ text: own,
100
+ editable: plain
101
+ });
102
+ lastText = own;
103
+ }
104
+ } else if (componentId === 'markdown' && typeof props['content'] === 'string') {
105
+ for (const line of String(props['content']).split('\n')){
106
+ const match = /^(#{1,6})\s+(.+)$/.exec(line.trim());
107
+ if (match) {
108
+ headings.push({
109
+ nodeId: null,
110
+ level: match[1].length,
111
+ text: collapse(match[2]),
112
+ editable: false
113
+ });
114
+ }
115
+ }
116
+ lastText = collapse(String(props['content'])).slice(0, IMAGE_CONTEXT_MAX_CHARS);
117
+ } else if (componentId === 'custom-html' && typeof ((_props_html = props['html']) != null ? _props_html : props['children']) === 'string') {
118
+ var _props_html1;
119
+ const html = String((_props_html1 = props['html']) != null ? _props_html1 : props['children']);
120
+ for (const match of html.matchAll(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi)){
121
+ headings.push({
122
+ nodeId: null,
123
+ level: Number(match[1]),
124
+ text: htmlText(match[2]),
125
+ editable: false
126
+ });
127
+ }
128
+ } else if (componentId === 'image') {
129
+ var _props_src;
130
+ const src = String((_props_src = props['src']) != null ? _props_src : '').trim();
131
+ if (src) {
132
+ var _props_alt;
133
+ images.push({
134
+ nodeId: id,
135
+ src,
136
+ alt: collapse(String((_props_alt = props['alt']) != null ? _props_alt : '')),
137
+ context: lastText.slice(0, IMAGE_CONTEXT_MAX_CHARS)
138
+ });
139
+ }
140
+ }
141
+ for (const childId of childIdsOf(node))visit(childId, depth + 1);
142
+ };
143
+ visit(contentRootId, 0);
144
+ return {
145
+ text,
146
+ wordCount: text ? text.split(/\s+/).filter(Boolean).length : 0,
147
+ headings,
148
+ h1s: headings.filter((heading)=>heading.level === 1),
149
+ images,
150
+ imagesMissingAlt: images.filter((image)=>!image.alt),
151
+ contentRootId
152
+ };
153
+ }
154
+
155
+ //# sourceMappingURL=seo-page-facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/seo-page-facts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n buildPageMarkdown,\n headingLevelOf,\n isPageChromeNode,\n pageContentRootId,\n type PageMarkdownNode,\n type PageMarkdownNodes,\n} from './page-markdown'\n\n/**\n * What a page SAYS, read off its node tree (AGL-2910): the text a search\n * listing summarizes, the headings, and the images — each heading and image\n * by node id, so a finding can name the element it is about and a fix can\n * address the element it changes.\n *\n * Pure, and the platform's: the SEO check (`seo-audit.ts`) judges a page by\n * these facts, and anything that proposes a listing or a content fix reads\n * the same ones rather than walking the tree its own way.\n *\n * The text is the page's Markdown representation, the one an agent is\n * served, so what the listing summarizes is what a reader of the page gets.\n * The walk starts where that serializer starts — the content region — and\n * skips what it skips, through the same chrome predicate: a heading in the\n * site's navigation is not the page's heading.\n */\n\n/** How much of a page's text the facts carry. A listing summarizes; it does not need the page. */\nexport const SEO_PAGE_TEXT_MAX_CHARS = 2_000\n\n/** How much text around an image describes it. */\nconst IMAGE_CONTEXT_MAX_CHARS = 160\n\n/** The deepest a walk goes, and it stops at a cycle — a node map is data. */\nconst MAX_DEPTH = 64\n\n/** The text components whose heading level `headingLevelOf` reads. */\nconst TEXT_COMPONENTS: ReadonlySet<string> = new Set(['muiTypography', 'muiInlineText', 'muiListItemText'])\n\nexport interface SeoHeading {\n /** The element, or `null` for a heading inside rich text a fix cannot address. */\n nodeId: string | null\n level: number\n text: string\n /** Whether a content fix can change it: a text component with plain text. */\n editable: boolean\n}\n\nexport interface SeoImage {\n nodeId: string\n src: string\n alt: string\n /** The nearest heading or text before it, which is what the image illustrates. */\n context: string\n}\n\nexport interface SeoPageFacts {\n /** The page's Markdown body, capped. */\n text: string\n wordCount: number\n headings: SeoHeading[]\n h1s: SeoHeading[]\n images: SeoImage[]\n imagesMissingAlt: SeoImage[]\n /** The node the content region starts at; a new heading goes first inside it. */\n contentRootId: string | null\n}\n\nconst collapse = (value: string): string => value.replace(/\\s+/g, ' ').trim()\n\n/** Author HTML to its text, for a heading stored as rich text. */\nfunction htmlText(html: string): string {\n return collapse(html.replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&'))\n}\n\n/** A text component's own text, and whether it is plain (a fix can rewrite it). */\nfunction textOf(props: Record<string, unknown>): { text: string; plain: boolean } {\n const html = props['html']\n if (typeof html === 'string' && html.trim()) return { text: htmlText(html), plain: false }\n const children = props['children']\n return typeof children === 'string' ? { text: collapse(children), plain: true } : { text: '', plain: true }\n}\n\nconst childIdsOf = (node: PageMarkdownNode | undefined): string[] =>\n Array.isArray(node?.nodes) ? (node?.nodes as unknown[]).filter((id): id is string => typeof id === 'string') : []\n\n/** Read the facts a listing and the SEO check need from one page's node map. */\nexport function seoPageFacts(\n nodes: PageMarkdownNodes | null | undefined,\n options: { rootId?: string; maxChars?: number } = {},\n): SeoPageFacts {\n const empty: SeoPageFacts = {\n text: '',\n wordCount: 0,\n headings: [],\n h1s: [],\n images: [],\n imagesMissingAlt: [],\n contentRootId: null,\n }\n if (!nodes) return empty\n const contentRootId = pageContentRootId(nodes, options.rootId)\n if (!contentRootId) return empty\n\n const markdown = buildPageMarkdown({ nodes, rootId: options.rootId })\n const text = markdown.trim().slice(0, options.maxChars ?? SEO_PAGE_TEXT_MAX_CHARS)\n const headings: SeoHeading[] = []\n const images: SeoImage[] = []\n let lastText = ''\n\n const seen = new Set<string>()\n const visit = (id: string, depth: number): void => {\n if (depth > MAX_DEPTH || seen.has(id)) return\n const node = nodes[id]\n if (!node) return\n seen.add(id)\n if (isPageChromeNode(node)) return\n const props = (node.props ?? {}) as Record<string, unknown>\n const componentId = String(node.componentId ?? '')\n if (TEXT_COMPONENTS.has(componentId)) {\n const { text: own, plain } = textOf(props)\n const level = headingLevelOf(props)\n if (own) {\n if (level > 0) headings.push({ nodeId: id, level, text: own, editable: plain })\n lastText = own\n }\n } else if (componentId === 'markdown' && typeof props['content'] === 'string') {\n for (const line of String(props['content']).split('\\n')) {\n const match = /^(#{1,6})\\s+(.+)$/.exec(line.trim())\n if (match) {\n headings.push({ nodeId: null, level: match[1].length, text: collapse(match[2]), editable: false })\n }\n }\n lastText = collapse(String(props['content'])).slice(0, IMAGE_CONTEXT_MAX_CHARS)\n } else if (componentId === 'custom-html' && typeof (props['html'] ?? props['children']) === 'string') {\n const html = String(props['html'] ?? props['children'])\n for (const match of html.matchAll(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi)) {\n headings.push({ nodeId: null, level: Number(match[1]), text: htmlText(match[2]), editable: false })\n }\n } else if (componentId === 'image') {\n const src = String(props['src'] ?? '').trim()\n if (src) {\n images.push({\n nodeId: id,\n src,\n alt: collapse(String(props['alt'] ?? '')),\n context: lastText.slice(0, IMAGE_CONTEXT_MAX_CHARS),\n })\n }\n }\n for (const childId of childIdsOf(node)) visit(childId, depth + 1)\n }\n visit(contentRootId, 0)\n\n return {\n text,\n wordCount: text ? text.split(/\\s+/).filter(Boolean).length : 0,\n headings,\n h1s: headings.filter((heading) => heading.level === 1),\n images,\n imagesMissingAlt: images.filter((image) => !image.alt),\n contentRootId,\n }\n}\n"],"names":["buildPageMarkdown","headingLevelOf","isPageChromeNode","pageContentRootId","SEO_PAGE_TEXT_MAX_CHARS","IMAGE_CONTEXT_MAX_CHARS","MAX_DEPTH","TEXT_COMPONENTS","Set","collapse","value","replace","trim","htmlText","html","textOf","props","text","plain","children","childIdsOf","node","Array","isArray","nodes","filter","id","seoPageFacts","options","empty","wordCount","headings","h1s","images","imagesMissingAlt","contentRootId","rootId","markdown","slice","maxChars","lastText","seen","visit","depth","has","add","componentId","String","own","level","push","nodeId","editable","line","split","match","exec","length","matchAll","Number","src","alt","context","childId","Boolean","heading","image"],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type SeoAuditPage, type SeoAuditSite } from './seo-audit';
18
+ /**
19
+ * Read a site for the SEO check (`seo-audit.ts`) the way its sitemap lists
20
+ * it: every screen the routing map publishes, less the template screens, the
21
+ * status pages and every screen that is not public — through the same shared
22
+ * predicates the tenant's `sitemap.xml` and `/llms.txt` apply, so the check
23
+ * covers exactly the pages a crawler is handed. Then each page's published
24
+ * version, and the shared layouts, for the links that decide which pages
25
+ * nothing points to.
26
+ *
27
+ * It knows nothing about the Firestore SDK. A caller hands it a
28
+ * {@link SeoScanStore} — the Admin SDK's `Firestore` satisfies it — and the
29
+ * template screen ids, which the tenant runtime resolves; so this module stays
30
+ * pure enough to import from anywhere, and a spec hands it a fake.
31
+ *
32
+ * Reads: one projected query of the screens, one of the layouts, and one
33
+ * version document per checked page and per layout — at most
34
+ * {@link SEO_AUDIT_MAX_PAGES} + {@link SEO_SCAN_LAYOUT_LIMIT} + 2.
35
+ */
36
+ /** Shared layouts read for the links they carry. */
37
+ export declare const SEO_SCAN_LAYOUT_LIMIT = 20;
38
+ export interface SeoScanSnapshot {
39
+ readonly exists: boolean;
40
+ get(field: string): unknown;
41
+ }
42
+ export interface SeoScanQueryDocument {
43
+ readonly id: string;
44
+ data(): unknown;
45
+ get(field: string): unknown;
46
+ readonly ref: SeoScanDocumentRef;
47
+ }
48
+ export interface SeoScanQuery {
49
+ limit(count: number): SeoScanQuery;
50
+ get(): Promise<{
51
+ readonly docs: readonly SeoScanQueryDocument[];
52
+ }>;
53
+ }
54
+ export interface SeoScanCollection {
55
+ select(...fields: string[]): SeoScanQuery;
56
+ doc(id: string): SeoScanDocumentRef;
57
+ }
58
+ export interface SeoScanDocumentRef {
59
+ collection(name: string): SeoScanCollection;
60
+ get(): Promise<SeoScanSnapshot>;
61
+ }
62
+ /** What the scan reads through; the Admin SDK's `Firestore` is one. */
63
+ export interface SeoScanStore {
64
+ collection(name: string): SeoScanCollection;
65
+ }
66
+ /** The host document fields the scan and the site findings read. */
67
+ export interface SeoSiteHost {
68
+ displayName?: string;
69
+ screens?: Record<string, string>;
70
+ errorScreens?: Record<string, string>;
71
+ notFoundScreenId?: string | null;
72
+ seo?: {
73
+ title?: string;
74
+ separator?: string;
75
+ discourageSearchEngines?: boolean;
76
+ entity?: {
77
+ name?: string;
78
+ description?: string;
79
+ };
80
+ agent?: {
81
+ whenToUse?: string;
82
+ };
83
+ };
84
+ }
85
+ /** The screen document fields the scan reads. */
86
+ interface SeoScreenDocument {
87
+ displayName?: string;
88
+ description?: string;
89
+ versionId?: string;
90
+ visibility?: unknown;
91
+ deletedAt?: unknown;
92
+ seo?: {
93
+ title?: string;
94
+ description?: string;
95
+ breadcrumb?: string;
96
+ image?: string;
97
+ imageAlt?: string;
98
+ };
99
+ }
100
+ export interface SeoScannedPage extends SeoAuditPage {
101
+ /** The listing as the screen document stores it: a title's variables unresolved. */
102
+ storedSeo: NonNullable<SeoScreenDocument['seo']>;
103
+ /** The page's node map as checked. */
104
+ nodes: Record<string, unknown> | null;
105
+ }
106
+ export interface SeoSiteScan {
107
+ pages: SeoScannedPage[];
108
+ /** Published pages past {@link SEO_AUDIT_MAX_PAGES}. */
109
+ skipped: number;
110
+ /** Customer-safe lines: a keyword line naming a page the site does not publish. */
111
+ notes: string[];
112
+ }
113
+ /** The site as the check reads it, from its host document. */
114
+ export declare function seoAuditSiteOf(host: SeoSiteHost): SeoAuditSite;
115
+ /**
116
+ * Read `hostId`'s published pages for the check.
117
+ *
118
+ * `keywords` are the target keyword lines a person typed (`/path: a, b`);
119
+ * `templateScreenIds` are the screens a collection, the store or the host
120
+ * renders other URLs through, which are not pages of their own.
121
+ */
122
+ export declare function scanSeoSite(store: SeoScanStore, hostId: string, host: SeoSiteHost, options?: {
123
+ keywords?: unknown;
124
+ templateScreenIds?: Iterable<string>;
125
+ }): Promise<SeoSiteScan>;
126
+ export {};
@@ -0,0 +1,162 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { screenRoutePathToUrl } from "./screen-route.js";
18
+ import { nodesReferenceScreen } from "./screen-link-value.js";
19
+ import { isScreenIndexable, isSearchDiscouraged, statusPageScreenIds } from "./search-indexing.js";
20
+ import { SEO_AUDIT_MAX_PAGES, parseSeoKeywordLines, seoNormalizePath } from "./seo-audit.js";
21
+ import { seoPageFacts } from "./seo-page-facts.js";
22
+ import { resolveSeoTitleVariables } from "./seo-title-variables.js";
23
+ import { decodeStoredNodes } from "./stored-nodes.js";
24
+ /**
25
+ * Read a site for the SEO check (`seo-audit.ts`) the way its sitemap lists
26
+ * it: every screen the routing map publishes, less the template screens, the
27
+ * status pages and every screen that is not public — through the same shared
28
+ * predicates the tenant's `sitemap.xml` and `/llms.txt` apply, so the check
29
+ * covers exactly the pages a crawler is handed. Then each page's published
30
+ * version, and the shared layouts, for the links that decide which pages
31
+ * nothing points to.
32
+ *
33
+ * It knows nothing about the Firestore SDK. A caller hands it a
34
+ * {@link SeoScanStore} — the Admin SDK's `Firestore` satisfies it — and the
35
+ * template screen ids, which the tenant runtime resolves; so this module stays
36
+ * pure enough to import from anywhere, and a spec hands it a fake.
37
+ *
38
+ * Reads: one projected query of the screens, one of the layouts, and one
39
+ * version document per checked page and per layout — at most
40
+ * {@link SEO_AUDIT_MAX_PAGES} + {@link SEO_SCAN_LAYOUT_LIMIT} + 2.
41
+ */ /** Shared layouts read for the links they carry. */ export const SEO_SCAN_LAYOUT_LIMIT = 20;
42
+ /** Version documents read at once. */ const READ_CHUNK = 10;
43
+ /** Screen documents the projected query reads. */ const SCREEN_SCAN_LIMIT = 1000;
44
+ const str = (value)=>typeof value === 'string' ? value.trim() : '';
45
+ /** The site as the check reads it, from its host document. */ export function seoAuditSiteOf(host) {
46
+ var _ref, _ref1;
47
+ var _host_seo, _host_seo1;
48
+ return {
49
+ discouraged: isSearchDiscouraged(host),
50
+ entity: (_ref = (_host_seo = host.seo) == null ? void 0 : _host_seo.entity) != null ? _ref : {},
51
+ agent: (_ref1 = (_host_seo1 = host.seo) == null ? void 0 : _host_seo1.agent) != null ? _ref1 : {}
52
+ };
53
+ }
54
+ /** A version's node map and root, or `null` when it cannot be read. */ async function readVersionNodes(parent, versionId) {
55
+ if (!versionId) return null;
56
+ const snapshot = await parent.collection('versions').doc(versionId).get();
57
+ if (!snapshot.exists) return null;
58
+ const nodes = decodeStoredNodes(snapshot.get('nodes'));
59
+ if (!nodes) return null;
60
+ const rootId = snapshot.get('rootId');
61
+ return _extends({
62
+ nodes
63
+ }, typeof rootId === 'string' ? {
64
+ rootId
65
+ } : {});
66
+ }
67
+ async function inChunks(items, run) {
68
+ for(let start = 0; start < items.length; start += READ_CHUNK){
69
+ await Promise.all(items.slice(start, start + READ_CHUNK).map(run));
70
+ }
71
+ }
72
+ /**
73
+ * Read `hostId`'s published pages for the check.
74
+ *
75
+ * `keywords` are the target keyword lines a person typed (`/path: a, b`);
76
+ * `templateScreenIds` are the screens a collection, the store or the host
77
+ * renders other URLs through, which are not pages of their own.
78
+ */ export async function scanSeoSite(store, hostId, host, options = {}) {
79
+ var _host_screens, _options_templateScreenIds;
80
+ var _host_seo;
81
+ const hostRef = store.collection('hosts').doc(hostId);
82
+ const routing = (_host_screens = host.screens) != null ? _host_screens : {};
83
+ const [screenDocs, layoutDocs] = await Promise.all([
84
+ hostRef.collection('screens').select('visibility', 'seo', 'description', 'displayName', 'versionId', 'deletedAt').limit(SCREEN_SCAN_LIMIT).get(),
85
+ hostRef.collection('layouts').select('versionId').limit(SEO_SCAN_LAYOUT_LIMIT).get()
86
+ ]);
87
+ const screens = new Map(screenDocs.docs.map((doc)=>{
88
+ var _doc_data;
89
+ return [
90
+ doc.id,
91
+ (_doc_data = doc.data()) != null ? _doc_data : {}
92
+ ];
93
+ }));
94
+ const excluded = new Set([
95
+ ...statusPageScreenIds(host),
96
+ ...(_options_templateScreenIds = options.templateScreenIds) != null ? _options_templateScreenIds : []
97
+ ]);
98
+ const published = Object.entries(routing).filter(([screenId])=>!excluded.has(screenId)).filter(([screenId])=>{
99
+ const screen = screens.get(screenId);
100
+ return Boolean(screen) && !(screen == null ? void 0 : screen.deletedAt) && isScreenIndexable(screen);
101
+ }).map(([screenId, route])=>({
102
+ screenId,
103
+ path: screenRoutePathToUrl(String(route))
104
+ })).sort((a, b)=>a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
105
+ const checked = published.slice(0, SEO_AUDIT_MAX_PAGES);
106
+ const versions = new Map();
107
+ await inChunks(checked, async ({ screenId })=>{
108
+ var _screens_get;
109
+ versions.set(screenId, await readVersionNodes(hostRef.collection('screens').doc(screenId), str((_screens_get = screens.get(screenId)) == null ? void 0 : _screens_get.versionId)));
110
+ });
111
+ const layoutNodes = [];
112
+ await inChunks(layoutDocs.docs.filter((doc)=>str(doc.get('versionId'))), async (doc)=>{
113
+ const version = await readVersionNodes(doc.ref, str(doc.get('versionId')));
114
+ if (version) layoutNodes.push(version.nodes);
115
+ });
116
+ const keywordsByPath = parseSeoKeywordLines(options.keywords);
117
+ const checkedPaths = new Set(checked.map((page)=>seoNormalizePath(page.path)));
118
+ const notes = Object.keys(keywordsByPath).filter((path)=>!checkedPaths.has(path)).map((path)=>`Keywords for ${path} were not used: no checked page is published at that address.`);
119
+ const siteName = str((_host_seo = host.seo) == null ? void 0 : _host_seo.title) || str(host.displayName);
120
+ const pages = checked.map(({ screenId, path })=>{
121
+ var _screens_get, _versions_get, _screen_seo, _keywordsByPath_seoNormalizePath, _ref;
122
+ var _host_seo;
123
+ const screen = (_screens_get = screens.get(screenId)) != null ? _screens_get : {};
124
+ const version = (_versions_get = versions.get(screenId)) != null ? _versions_get : null;
125
+ const storedSeo = (_screen_seo = screen.seo) != null ? _screen_seo : {};
126
+ const linkedFrom = layoutNodes.some((nodes)=>nodesReferenceScreen(nodes, screenId)) || checked.some((other)=>{
127
+ var _ref;
128
+ var _versions_get;
129
+ return other.screenId !== screenId && nodesReferenceScreen((_ref = (_versions_get = versions.get(other.screenId)) == null ? void 0 : _versions_get.nodes) != null ? _ref : null, screenId);
130
+ });
131
+ return {
132
+ screenId,
133
+ path,
134
+ name: str(screen.displayName) || path,
135
+ versionId: str(screen.versionId) || null,
136
+ // Judged as the head renders it: `{{page.name}} – {{site.name}}` on two
137
+ // pages is two different titles, and its length is the rendered one.
138
+ seo: _extends({}, storedSeo, {
139
+ title: resolveSeoTitleVariables(storedSeo.title, {
140
+ 'page.name': str(screen.displayName),
141
+ 'site.name': siteName,
142
+ 'site.separator': str((_host_seo = host.seo) == null ? void 0 : _host_seo.separator)
143
+ })
144
+ }),
145
+ storedSeo,
146
+ description: str(screen.description),
147
+ facts: seoPageFacts(version == null ? void 0 : version.nodes, {
148
+ rootId: version == null ? void 0 : version.rootId
149
+ }),
150
+ linkedFrom,
151
+ keywords: (_keywordsByPath_seoNormalizePath = keywordsByPath[seoNormalizePath(path)]) != null ? _keywordsByPath_seoNormalizePath : [],
152
+ nodes: (_ref = version == null ? void 0 : version.nodes) != null ? _ref : null
153
+ };
154
+ });
155
+ return {
156
+ pages,
157
+ skipped: Math.max(0, published.length - checked.length),
158
+ notes
159
+ };
160
+ }
161
+
162
+ //# sourceMappingURL=seo-site-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/seo-site-scan.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 { screenRoutePathToUrl } from './screen-route'\nimport { nodesReferenceScreen } from './screen-link-value'\nimport {\n isScreenIndexable,\n isSearchDiscouraged,\n statusPageScreenIds,\n type SearchIndexingScreen,\n} from './search-indexing'\nimport {\n SEO_AUDIT_MAX_PAGES,\n parseSeoKeywordLines,\n seoNormalizePath,\n type SeoAuditPage,\n type SeoAuditSite,\n} from './seo-audit'\nimport { seoPageFacts } from './seo-page-facts'\nimport type { PageMarkdownNodes } from './page-markdown'\nimport { resolveSeoTitleVariables } from './seo-title-variables'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/**\n * Read a site for the SEO check (`seo-audit.ts`) the way its sitemap lists\n * it: every screen the routing map publishes, less the template screens, the\n * status pages and every screen that is not public — through the same shared\n * predicates the tenant's `sitemap.xml` and `/llms.txt` apply, so the check\n * covers exactly the pages a crawler is handed. Then each page's published\n * version, and the shared layouts, for the links that decide which pages\n * nothing points to.\n *\n * It knows nothing about the Firestore SDK. A caller hands it a\n * {@link SeoScanStore} — the Admin SDK's `Firestore` satisfies it — and the\n * template screen ids, which the tenant runtime resolves; so this module stays\n * pure enough to import from anywhere, and a spec hands it a fake.\n *\n * Reads: one projected query of the screens, one of the layouts, and one\n * version document per checked page and per layout — at most\n * {@link SEO_AUDIT_MAX_PAGES} + {@link SEO_SCAN_LAYOUT_LIMIT} + 2.\n */\n\n/** Shared layouts read for the links they carry. */\nexport const SEO_SCAN_LAYOUT_LIMIT = 20\n\n/** Version documents read at once. */\nconst READ_CHUNK = 10\n\n/** Screen documents the projected query reads. */\nconst SCREEN_SCAN_LIMIT = 1000\n\n/* ---- The store, structurally ---------------------------------------------- */\n\nexport interface SeoScanSnapshot {\n readonly exists: boolean\n get(field: string): unknown\n}\n\nexport interface SeoScanQueryDocument {\n readonly id: string\n data(): unknown\n get(field: string): unknown\n readonly ref: SeoScanDocumentRef\n}\n\nexport interface SeoScanQuery {\n limit(count: number): SeoScanQuery\n get(): Promise<{ readonly docs: readonly SeoScanQueryDocument[] }>\n}\n\nexport interface SeoScanCollection {\n select(...fields: string[]): SeoScanQuery\n doc(id: string): SeoScanDocumentRef\n}\n\nexport interface SeoScanDocumentRef {\n collection(name: string): SeoScanCollection\n get(): Promise<SeoScanSnapshot>\n}\n\n/** What the scan reads through; the Admin SDK's `Firestore` is one. */\nexport interface SeoScanStore {\n collection(name: string): SeoScanCollection\n}\n\n/* ---- What it reads --------------------------------------------------------- */\n\n/** The host document fields the scan and the site findings read. */\nexport interface SeoSiteHost {\n displayName?: string\n screens?: Record<string, string>\n errorScreens?: Record<string, string>\n notFoundScreenId?: string | null\n seo?: {\n title?: string\n separator?: string\n discourageSearchEngines?: boolean\n entity?: { name?: string; description?: string }\n agent?: { whenToUse?: string }\n }\n}\n\n/** The screen document fields the scan reads. */\ninterface SeoScreenDocument {\n displayName?: string\n description?: string\n versionId?: string\n visibility?: unknown\n deletedAt?: unknown\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n image?: string\n imageAlt?: string\n }\n}\n\nexport interface SeoScannedPage extends SeoAuditPage {\n /** The listing as the screen document stores it: a title's variables unresolved. */\n storedSeo: NonNullable<SeoScreenDocument['seo']>\n /** The page's node map as checked. */\n nodes: Record<string, unknown> | null\n}\n\nexport interface SeoSiteScan {\n pages: SeoScannedPage[]\n /** Published pages past {@link SEO_AUDIT_MAX_PAGES}. */\n skipped: number\n /** Customer-safe lines: a keyword line naming a page the site does not publish. */\n notes: string[]\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The site as the check reads it, from its host document. */\nexport function seoAuditSiteOf(host: SeoSiteHost): SeoAuditSite {\n return {\n discouraged: isSearchDiscouraged(host),\n entity: host.seo?.entity ?? {},\n agent: host.seo?.agent ?? {},\n }\n}\n\n/** A version's node map and root, or `null` when it cannot be read. */\nasync function readVersionNodes(\n parent: SeoScanDocumentRef,\n versionId: string,\n): Promise<{ nodes: Record<string, unknown>; rootId?: string } | null> {\n if (!versionId) return null\n const snapshot = await parent.collection('versions').doc(versionId).get()\n if (!snapshot.exists) return null\n const nodes = decodeStoredNodes<Record<string, unknown>>(snapshot.get('nodes'))\n if (!nodes) return null\n const rootId = snapshot.get('rootId')\n return { nodes, ...(typeof rootId === 'string' ? { rootId } : {}) }\n}\n\nasync function inChunks<T>(items: readonly T[], run: (item: T) => Promise<void>): Promise<void> {\n for (let start = 0; start < items.length; start += READ_CHUNK) {\n await Promise.all(items.slice(start, start + READ_CHUNK).map(run))\n }\n}\n\n/**\n * Read `hostId`'s published pages for the check.\n *\n * `keywords` are the target keyword lines a person typed (`/path: a, b`);\n * `templateScreenIds` are the screens a collection, the store or the host\n * renders other URLs through, which are not pages of their own.\n */\nexport async function scanSeoSite(\n store: SeoScanStore,\n hostId: string,\n host: SeoSiteHost,\n options: { keywords?: unknown; templateScreenIds?: Iterable<string> } = {},\n): Promise<SeoSiteScan> {\n const hostRef = store.collection('hosts').doc(hostId)\n const routing = host.screens ?? {}\n const [screenDocs, layoutDocs] = await Promise.all([\n hostRef\n .collection('screens')\n .select('visibility', 'seo', 'description', 'displayName', 'versionId', 'deletedAt')\n .limit(SCREEN_SCAN_LIMIT)\n .get(),\n hostRef.collection('layouts').select('versionId').limit(SEO_SCAN_LAYOUT_LIMIT).get(),\n ])\n const screens = new Map(screenDocs.docs.map((doc) => [doc.id, (doc.data() ?? {}) as SeoScreenDocument]))\n const excluded = new Set<string>([...statusPageScreenIds(host), ...(options.templateScreenIds ?? [])])\n const published = Object.entries(routing)\n .filter(([screenId]) => !excluded.has(screenId))\n .filter(([screenId]) => {\n const screen = screens.get(screenId)\n return Boolean(screen) && !screen?.deletedAt && isScreenIndexable(screen as SearchIndexingScreen)\n })\n .map(([screenId, route]) => ({ screenId, path: screenRoutePathToUrl(String(route)) }))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))\n const checked = published.slice(0, SEO_AUDIT_MAX_PAGES)\n\n const versions = new Map<string, { nodes: Record<string, unknown>; rootId?: string } | null>()\n await inChunks(checked, async ({ screenId }) => {\n versions.set(\n screenId,\n await readVersionNodes(hostRef.collection('screens').doc(screenId), str(screens.get(screenId)?.versionId)),\n )\n })\n const layoutNodes: Record<string, unknown>[] = []\n await inChunks(\n layoutDocs.docs.filter((doc) => str(doc.get('versionId'))),\n async (doc) => {\n const version = await readVersionNodes(doc.ref, str(doc.get('versionId')))\n if (version) layoutNodes.push(version.nodes)\n },\n )\n\n const keywordsByPath = parseSeoKeywordLines(options.keywords)\n const checkedPaths = new Set(checked.map((page) => seoNormalizePath(page.path)))\n const notes = Object.keys(keywordsByPath)\n .filter((path) => !checkedPaths.has(path))\n .map((path) => `Keywords for ${path} were not used: no checked page is published at that address.`)\n\n const siteName = str(host.seo?.title) || str(host.displayName)\n const pages: SeoScannedPage[] = checked.map(({ screenId, path }) => {\n const screen = screens.get(screenId) ?? {}\n const version = versions.get(screenId) ?? null\n const storedSeo = screen.seo ?? {}\n const linkedFrom =\n layoutNodes.some((nodes) => nodesReferenceScreen(nodes, screenId)) ||\n checked.some(\n (other) =>\n other.screenId !== screenId &&\n nodesReferenceScreen(versions.get(other.screenId)?.nodes ?? null, screenId),\n )\n return {\n screenId,\n path,\n name: str(screen.displayName) || path,\n versionId: str(screen.versionId) || null,\n // Judged as the head renders it: `{{page.name}} – {{site.name}}` on two\n // pages is two different titles, and its length is the rendered one.\n seo: {\n ...storedSeo,\n title: resolveSeoTitleVariables(storedSeo.title, {\n 'page.name': str(screen.displayName),\n 'site.name': siteName,\n 'site.separator': str(host.seo?.separator),\n }),\n },\n storedSeo,\n description: str(screen.description),\n facts: seoPageFacts(version?.nodes as PageMarkdownNodes | undefined, { rootId: version?.rootId }),\n linkedFrom,\n keywords: keywordsByPath[seoNormalizePath(path)] ?? [],\n nodes: version?.nodes ?? null,\n }\n })\n return { pages, skipped: Math.max(0, published.length - checked.length), notes }\n}\n"],"names":["screenRoutePathToUrl","nodesReferenceScreen","isScreenIndexable","isSearchDiscouraged","statusPageScreenIds","SEO_AUDIT_MAX_PAGES","parseSeoKeywordLines","seoNormalizePath","seoPageFacts","resolveSeoTitleVariables","decodeStoredNodes","SEO_SCAN_LAYOUT_LIMIT","READ_CHUNK","SCREEN_SCAN_LIMIT","str","value","trim","seoAuditSiteOf","host","discouraged","entity","seo","agent","readVersionNodes","parent","versionId","snapshot","collection","doc","get","exists","nodes","rootId","inChunks","items","run","start","length","Promise","all","slice","map","scanSeoSite","store","hostId","options","hostRef","routing","screens","screenDocs","layoutDocs","select","limit","Map","docs","id","data","excluded","Set","templateScreenIds","published","Object","entries","filter","screenId","has","screen","Boolean","deletedAt","route","path","String","sort","a","b","checked","versions","set","layoutNodes","version","ref","push","keywordsByPath","keywords","checkedPaths","page","notes","keys","siteName","title","displayName","pages","storedSeo","linkedFrom","some","other","name","separator","description","facts","skipped","Math","max"],"mappings":"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"}
@@ -71,7 +71,7 @@
71
71
  {
72
72
  name: 'page.name',
73
73
  label: 'Page name',
74
- description: 'What this page is called — a screen’s display name.'
74
+ description: 'What this page is called — its display name.'
75
75
  },
76
76
  {
77
77
  name: 'site.separator',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/seo-title-variables.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\n/**\n * The variables a page title may be written in terms of, and how a written\n * title becomes a rendered one.\n *\n * ## Why this exists\n *\n * A title is verbatim (AGL-1341, and that rule is right — see `seo-title`), so\n * an author who wants the brand in a title TYPES it. Fifty screens on the\n * marketing site end in a literal ` | Aglyn`, which means the site name and\n * the character in front of it are stored fifty times, and changing either one\n * is fifty edits that nothing reminds you to make. `seo.separator` already\n * exists as a setting and every one of those titles ignores it.\n *\n * So a title may now be WRITTEN in terms of what it depends on:\n *\n * ```\n * {{page.name}} {{site.separator}} {{site.name}}\n * ```\n *\n * and a site-name change is one edit in Host setup → SEO.\n *\n * ## ⛔ No rendered title changes because this shipped\n *\n * The whole feature is opt-in at the string level: a title with no `{{` in it\n * is returned unchanged, character for character, by the same code path that\n * returned it before. Nothing migrates the fifty existing titles — a migration\n * could not be output-preserving anyway, because ` | Aglyn` is not what\n * `{{site.separator}}{{site.name}}` resolves to on a host whose separator is\n * `-` and whose site title is the long form. They keep working as literals\n * until somebody decides otherwise, one at a time.\n *\n * `DEFAULT_TITLE_PATTERN` is held to the same standard: it is the composition\n * `resolveSeoTitle` already performed for a page with no authored title, said\n * in tokens instead of in code, and `seo-title.spec` pins that the two agree.\n *\n * ## The grammar is `{{…}}`, not `[…]`\n *\n * Because the platform already has one: the besigner's attribute fields, the\n * markdown blocks and the campaign editors all serialize `{{…}}` and the\n * people writing SEO titles are the same people who write those. A second\n * grammar for the same idea is a second thing to learn and a second parser to\n * keep in step.\n *\n * ## What is NOT a variable here\n *\n * Anything per-visitor or per-request. A `<title>` is rendered into a cached,\n * shared HTML document and read by crawlers, so a variable that resolved to\n * something about the reader would be wrong for everyone it was cached for.\n * These three are properties of the page and the site, both of which are the\n * same for everybody who asks for that URL.\n */\n\n/** A variable's name, as it is written between the braces. */\nexport type SeoTitleVariableName = 'page.name' | 'site.name' | 'site.separator'\n\nexport interface SeoTitleVariableDefinition {\n name: SeoTitleVariableName\n /** What the insert menu calls it. */\n label: string\n /** One line under the label, saying where the value comes from. */\n description: string\n}\n\n/**\n * Every variable a title may use, in the order a picker should offer them —\n * which is the order they appear in the default pattern, so the menu reads as\n * the sentence it builds.\n */\nexport const SEO_TITLE_VARIABLES: readonly SeoTitleVariableDefinition[] = [\n {\n name: 'page.name',\n label: 'Page name',\n description: 'What this page is called — a screen’s display name.',\n },\n {\n name: 'site.separator',\n label: 'Separator',\n description: 'The character from Host setup → SEO that joins the two.',\n },\n {\n name: 'site.name',\n label: 'Site name',\n description: 'The site title from Host setup → SEO.',\n },\n]\n\n/**\n * The composition every untitled page gets when the site sets no pattern of\n * its own.\n *\n * This is `resolveSeoTitle`'s previous hard-coded composition, written out:\n * name, separator padded by a space on each side, site title. It is a constant\n * rather than a string built at the call site so that the setting's default,\n * the placeholder the editor shows and the value the renderer falls back to\n * are one thing.\n */\nexport const DEFAULT_TITLE_PATTERN = '{{page.name}} {{site.separator}} {{site.name}}'\n\n/** The values the variables stand for, for one page of one site. */\nexport interface SeoTitleVariableValues {\n 'page.name'?: string | null\n 'site.name'?: string | null\n 'site.separator'?: string | null\n}\n\n/** Trimmed, or `''` for anything that is not a live string. */\nfunction clean(value?: string | null): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/**\n * The separator a site uses when it has set none.\n *\n * An en dash, which is what `seo-title` has padded around since AGL-1341. It\n * lives here because `{{site.separator}}` has to resolve to the SAME character\n * the code composed with — a variable that stood for \"the separator, unless\n * it is unset, in which case nothing\" would silently change the title of every\n * untitled page on every site that never filled the field in.\n */\nexport const DEFAULT_TITLE_SEPARATOR = '\\u2013'\n\n/** The site's separator, defaulted the one way everything defaults it. */\nexport function seoSeparatorOrDefault(separator?: string | null): string {\n return clean(separator) || DEFAULT_TITLE_SEPARATOR\n}\n\n/**\n * Whether a written value uses any variable at all.\n *\n * The cheap gate in front of every path here: a title with no `{{` is returned\n * by identity, which is what makes \"nothing renders differently\" a property of\n * the code rather than a claim about the data.\n */\nexport function hasSeoTitleVariables(value?: string | null): boolean {\n return typeof value === 'string' && value.includes('{{')\n}\n\n/** Whether a name is one this catalog defines. */\nexport function isSeoTitleVariable(name: string): name is SeoTitleVariableName {\n return SEO_TITLE_VARIABLES.some((variable) => variable.name === name)\n}\n\n/**\n * `{{ name }}` — the whole token, and the name inside it with its padding\n * trimmed, so a human who typed a space after the braces is not punished for\n * it.\n */\nconst TOKEN = /\\{\\{\\s*([a-zA-Z][\\w.]*)\\s*\\}\\}/g\n\ninterface Part {\n /** What renders here, once the variable is resolved. */\n text: string\n /** The variable this came from, if it came from one. */\n variable?: string\n}\n\n/** The written string as literal runs and variable slots, in order. */\nfunction partsOf(source: string, values: SeoTitleVariableValues): Part[] {\n const parts: Part[] = []\n let at = 0\n TOKEN.lastIndex = 0\n for (let match = TOKEN.exec(source); match; match = TOKEN.exec(source)) {\n if (match.index > at) parts.push({ text: source.slice(at, match.index) })\n const name = match[1]\n parts.push({\n variable: name,\n // An unknown variable renders AS ITSELF — see the resolver's docblock.\n text: !isSeoTitleVariable(name)\n ? match[0]\n : name === 'site.separator'\n ? seoSeparatorOrDefault(values[name])\n : clean(values[name]),\n })\n at = match.index + match[0].length\n }\n if (at < source.length) parts.push({ text: source.slice(at) })\n return parts\n}\n\n/**\n * Resolve a written title into the one that is rendered.\n *\n * ## An unknown variable is left alone\n *\n * `{{page.subtitle}}` renders as itself, visibly, rather than as an empty gap.\n * A typo that vanished would produce a title that is silently missing a piece,\n * and the author's next look at the live page would show them a sentence that\n * merely reads a bit short. Leaving it visible is the version somebody\n * notices. The editor's picker and autocomplete are what keep it from\n * happening; this is what happens when they are bypassed.\n *\n * ## A separator needs something on BOTH sides\n *\n * This is `resolveSeoTitle`'s own rule (AGL-1341) and it survives the move into\n * a pattern, because it is the rule that stops `<title>` from beginning with a\n * dash on a page whose name is missing. A `{{site.separator}}` with nothing\n * rendered on one side of it is dropped, whatever order the pattern put the\n * pieces in — which is why the parts are walked rather than regex-substituted:\n * a substitution cannot see its neighbours.\n *\n * ## An empty variable takes its spacing with it\n *\n * Otherwise a site with no name set renders `Pricing ` — the gap where the\n * missing piece was, and a trailing separator's worth of whitespace. Runs\n * collapse to one space and the result is trimmed.\n */\nexport function resolveSeoTitleVariables(\n written: string | null | undefined,\n values: SeoTitleVariableValues,\n): string {\n const source = typeof written === 'string' ? written : ''\n if (!hasSeoTitleVariables(source)) return source\n\n const parts = partsOf(source, values)\n const rendered = parts.map((part) => part.text)\n // A separator survives only where something renders on each side of it.\n parts.forEach((part, index) => {\n if (part.variable !== 'site.separator') return\n const live = (from: number, to: number) =>\n rendered.slice(from, to).some((text) => text.trim())\n if (!live(0, index) || !live(index + 1, rendered.length)) rendered[index] = ''\n })\n\n return rendered.join('').replace(/\\s{2,}/g, ' ').trim()\n}\n"],"names":["SEO_TITLE_VARIABLES","name","label","description","DEFAULT_TITLE_PATTERN","clean","value","trim","DEFAULT_TITLE_SEPARATOR","seoSeparatorOrDefault","separator","hasSeoTitleVariables","includes","isSeoTitleVariable","some","variable","TOKEN","partsOf","source","values","parts","at","lastIndex","match","exec","index","push","text","slice","length","resolveSeoTitleVariables","written","rendered","map","part","forEach","live","from","to","join","replace"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/seo-title-variables.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\n/**\n * The variables a page title may be written in terms of, and how a written\n * title becomes a rendered one.\n *\n * ## Why this exists\n *\n * A title is verbatim (AGL-1341, and that rule is right — see `seo-title`), so\n * an author who wants the brand in a title TYPES it. Fifty screens on the\n * marketing site end in a literal ` | Aglyn`, which means the site name and\n * the character in front of it are stored fifty times, and changing either one\n * is fifty edits that nothing reminds you to make. `seo.separator` already\n * exists as a setting and every one of those titles ignores it.\n *\n * So a title may now be WRITTEN in terms of what it depends on:\n *\n * ```\n * {{page.name}} {{site.separator}} {{site.name}}\n * ```\n *\n * and a site-name change is one edit in Host setup → SEO.\n *\n * ## ⛔ No rendered title changes because this shipped\n *\n * The whole feature is opt-in at the string level: a title with no `{{` in it\n * is returned unchanged, character for character, by the same code path that\n * returned it before. Nothing migrates the fifty existing titles — a migration\n * could not be output-preserving anyway, because ` | Aglyn` is not what\n * `{{site.separator}}{{site.name}}` resolves to on a host whose separator is\n * `-` and whose site title is the long form. They keep working as literals\n * until somebody decides otherwise, one at a time.\n *\n * `DEFAULT_TITLE_PATTERN` is held to the same standard: it is the composition\n * `resolveSeoTitle` already performed for a page with no authored title, said\n * in tokens instead of in code, and `seo-title.spec` pins that the two agree.\n *\n * ## The grammar is `{{…}}`, not `[…]`\n *\n * Because the platform already has one: the besigner's attribute fields, the\n * markdown blocks and the campaign editors all serialize `{{…}}` and the\n * people writing SEO titles are the same people who write those. A second\n * grammar for the same idea is a second thing to learn and a second parser to\n * keep in step.\n *\n * ## What is NOT a variable here\n *\n * Anything per-visitor or per-request. A `<title>` is rendered into a cached,\n * shared HTML document and read by crawlers, so a variable that resolved to\n * something about the reader would be wrong for everyone it was cached for.\n * These three are properties of the page and the site, both of which are the\n * same for everybody who asks for that URL.\n */\n\n/** A variable's name, as it is written between the braces. */\nexport type SeoTitleVariableName = 'page.name' | 'site.name' | 'site.separator'\n\nexport interface SeoTitleVariableDefinition {\n name: SeoTitleVariableName\n /** What the insert menu calls it. */\n label: string\n /** One line under the label, saying where the value comes from. */\n description: string\n}\n\n/**\n * Every variable a title may use, in the order a picker should offer them —\n * which is the order they appear in the default pattern, so the menu reads as\n * the sentence it builds.\n */\nexport const SEO_TITLE_VARIABLES: readonly SeoTitleVariableDefinition[] = [\n {\n name: 'page.name',\n label: 'Page name',\n description: 'What this page is called — its display name.',\n },\n {\n name: 'site.separator',\n label: 'Separator',\n description: 'The character from Host setup → SEO that joins the two.',\n },\n {\n name: 'site.name',\n label: 'Site name',\n description: 'The site title from Host setup → SEO.',\n },\n]\n\n/**\n * The composition every untitled page gets when the site sets no pattern of\n * its own.\n *\n * This is `resolveSeoTitle`'s previous hard-coded composition, written out:\n * name, separator padded by a space on each side, site title. It is a constant\n * rather than a string built at the call site so that the setting's default,\n * the placeholder the editor shows and the value the renderer falls back to\n * are one thing.\n */\nexport const DEFAULT_TITLE_PATTERN = '{{page.name}} {{site.separator}} {{site.name}}'\n\n/** The values the variables stand for, for one page of one site. */\nexport interface SeoTitleVariableValues {\n 'page.name'?: string | null\n 'site.name'?: string | null\n 'site.separator'?: string | null\n}\n\n/** Trimmed, or `''` for anything that is not a live string. */\nfunction clean(value?: string | null): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/**\n * The separator a site uses when it has set none.\n *\n * An en dash, which is what `seo-title` has padded around since AGL-1341. It\n * lives here because `{{site.separator}}` has to resolve to the SAME character\n * the code composed with — a variable that stood for \"the separator, unless\n * it is unset, in which case nothing\" would silently change the title of every\n * untitled page on every site that never filled the field in.\n */\nexport const DEFAULT_TITLE_SEPARATOR = '\\u2013'\n\n/** The site's separator, defaulted the one way everything defaults it. */\nexport function seoSeparatorOrDefault(separator?: string | null): string {\n return clean(separator) || DEFAULT_TITLE_SEPARATOR\n}\n\n/**\n * Whether a written value uses any variable at all.\n *\n * The cheap gate in front of every path here: a title with no `{{` is returned\n * by identity, which is what makes \"nothing renders differently\" a property of\n * the code rather than a claim about the data.\n */\nexport function hasSeoTitleVariables(value?: string | null): boolean {\n return typeof value === 'string' && value.includes('{{')\n}\n\n/** Whether a name is one this catalog defines. */\nexport function isSeoTitleVariable(name: string): name is SeoTitleVariableName {\n return SEO_TITLE_VARIABLES.some((variable) => variable.name === name)\n}\n\n/**\n * `{{ name }}` — the whole token, and the name inside it with its padding\n * trimmed, so a human who typed a space after the braces is not punished for\n * it.\n */\nconst TOKEN = /\\{\\{\\s*([a-zA-Z][\\w.]*)\\s*\\}\\}/g\n\ninterface Part {\n /** What renders here, once the variable is resolved. */\n text: string\n /** The variable this came from, if it came from one. */\n variable?: string\n}\n\n/** The written string as literal runs and variable slots, in order. */\nfunction partsOf(source: string, values: SeoTitleVariableValues): Part[] {\n const parts: Part[] = []\n let at = 0\n TOKEN.lastIndex = 0\n for (let match = TOKEN.exec(source); match; match = TOKEN.exec(source)) {\n if (match.index > at) parts.push({ text: source.slice(at, match.index) })\n const name = match[1]\n parts.push({\n variable: name,\n // An unknown variable renders AS ITSELF — see the resolver's docblock.\n text: !isSeoTitleVariable(name)\n ? match[0]\n : name === 'site.separator'\n ? seoSeparatorOrDefault(values[name])\n : clean(values[name]),\n })\n at = match.index + match[0].length\n }\n if (at < source.length) parts.push({ text: source.slice(at) })\n return parts\n}\n\n/**\n * Resolve a written title into the one that is rendered.\n *\n * ## An unknown variable is left alone\n *\n * `{{page.subtitle}}` renders as itself, visibly, rather than as an empty gap.\n * A typo that vanished would produce a title that is silently missing a piece,\n * and the author's next look at the live page would show them a sentence that\n * merely reads a bit short. Leaving it visible is the version somebody\n * notices. The editor's picker and autocomplete are what keep it from\n * happening; this is what happens when they are bypassed.\n *\n * ## A separator needs something on BOTH sides\n *\n * This is `resolveSeoTitle`'s own rule (AGL-1341) and it survives the move into\n * a pattern, because it is the rule that stops `<title>` from beginning with a\n * dash on a page whose name is missing. A `{{site.separator}}` with nothing\n * rendered on one side of it is dropped, whatever order the pattern put the\n * pieces in — which is why the parts are walked rather than regex-substituted:\n * a substitution cannot see its neighbours.\n *\n * ## An empty variable takes its spacing with it\n *\n * Otherwise a site with no name set renders `Pricing ` — the gap where the\n * missing piece was, and a trailing separator's worth of whitespace. Runs\n * collapse to one space and the result is trimmed.\n */\nexport function resolveSeoTitleVariables(\n written: string | null | undefined,\n values: SeoTitleVariableValues,\n): string {\n const source = typeof written === 'string' ? written : ''\n if (!hasSeoTitleVariables(source)) return source\n\n const parts = partsOf(source, values)\n const rendered = parts.map((part) => part.text)\n // A separator survives only where something renders on each side of it.\n parts.forEach((part, index) => {\n if (part.variable !== 'site.separator') return\n const live = (from: number, to: number) =>\n rendered.slice(from, to).some((text) => text.trim())\n if (!live(0, index) || !live(index + 1, rendered.length)) rendered[index] = ''\n })\n\n return rendered.join('').replace(/\\s{2,}/g, ' ').trim()\n}\n"],"names":["SEO_TITLE_VARIABLES","name","label","description","DEFAULT_TITLE_PATTERN","clean","value","trim","DEFAULT_TITLE_SEPARATOR","seoSeparatorOrDefault","separator","hasSeoTitleVariables","includes","isSeoTitleVariable","some","variable","TOKEN","partsOf","source","values","parts","at","lastIndex","match","exec","index","push","text","slice","length","resolveSeoTitleVariables","written","rendered","map","part","forEach","live","from","to","join","replace"],"mappings":"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"}