@actuate-media/cms-core 0.66.0 → 0.68.0

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 (148) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/__tests__/api/ai-quality.test.js +73 -0
  3. package/dist/__tests__/api/ai-quality.test.js.map +1 -1
  4. package/dist/__tests__/api/llms-txt.test.js +284 -0
  5. package/dist/__tests__/api/llms-txt.test.js.map +1 -1
  6. package/dist/__tests__/api/public-seo.test.js +402 -4
  7. package/dist/__tests__/api/public-seo.test.js.map +1 -1
  8. package/dist/__tests__/api/route-contracts.test.js +7 -0
  9. package/dist/__tests__/api/route-contracts.test.js.map +1 -1
  10. package/dist/__tests__/content/slug.test.d.ts +2 -0
  11. package/dist/__tests__/content/slug.test.d.ts.map +1 -0
  12. package/dist/__tests__/content/slug.test.js +68 -0
  13. package/dist/__tests__/content/slug.test.js.map +1 -0
  14. package/dist/__tests__/redirects/auto-301.test.d.ts +2 -0
  15. package/dist/__tests__/redirects/auto-301.test.d.ts.map +1 -0
  16. package/dist/__tests__/redirects/auto-301.test.js +172 -0
  17. package/dist/__tests__/redirects/auto-301.test.js.map +1 -0
  18. package/dist/__tests__/seo/ai-bots.test.d.ts +2 -0
  19. package/dist/__tests__/seo/ai-bots.test.d.ts.map +1 -0
  20. package/dist/__tests__/seo/ai-bots.test.js +53 -0
  21. package/dist/__tests__/seo/ai-bots.test.js.map +1 -0
  22. package/dist/__tests__/seo/alt-text.test.d.ts +2 -0
  23. package/dist/__tests__/seo/alt-text.test.d.ts.map +1 -0
  24. package/dist/__tests__/seo/alt-text.test.js +47 -0
  25. package/dist/__tests__/seo/alt-text.test.js.map +1 -0
  26. package/dist/__tests__/seo/feeds.test.d.ts +2 -0
  27. package/dist/__tests__/seo/feeds.test.d.ts.map +1 -0
  28. package/dist/__tests__/seo/feeds.test.js +61 -0
  29. package/dist/__tests__/seo/feeds.test.js.map +1 -0
  30. package/dist/__tests__/seo/freshness.test.d.ts +2 -0
  31. package/dist/__tests__/seo/freshness.test.d.ts.map +1 -0
  32. package/dist/__tests__/seo/freshness.test.js +89 -0
  33. package/dist/__tests__/seo/freshness.test.js.map +1 -0
  34. package/dist/__tests__/seo/llms-sanitize.test.d.ts +2 -0
  35. package/dist/__tests__/seo/llms-sanitize.test.d.ts.map +1 -0
  36. package/dist/__tests__/seo/llms-sanitize.test.js +74 -0
  37. package/dist/__tests__/seo/llms-sanitize.test.js.map +1 -0
  38. package/dist/__tests__/seo/meta-tags-alternates.test.d.ts +2 -0
  39. package/dist/__tests__/seo/meta-tags-alternates.test.d.ts.map +1 -0
  40. package/dist/__tests__/seo/meta-tags-alternates.test.js +52 -0
  41. package/dist/__tests__/seo/meta-tags-alternates.test.js.map +1 -0
  42. package/dist/__tests__/seo/page-meta.test.js +230 -14
  43. package/dist/__tests__/seo/page-meta.test.js.map +1 -1
  44. package/dist/__tests__/seo/sitemap-notify.test.js +77 -1
  45. package/dist/__tests__/seo/sitemap-notify.test.js.map +1 -1
  46. package/dist/__tests__/validation/publish.test.d.ts +2 -0
  47. package/dist/__tests__/validation/publish.test.d.ts.map +1 -0
  48. package/dist/__tests__/validation/publish.test.js +164 -0
  49. package/dist/__tests__/validation/publish.test.js.map +1 -0
  50. package/dist/actions.d.ts.map +1 -1
  51. package/dist/actions.js +172 -8
  52. package/dist/actions.js.map +1 -1
  53. package/dist/api/route-helpers.d.ts +8 -0
  54. package/dist/api/route-helpers.d.ts.map +1 -1
  55. package/dist/api/route-helpers.js +20 -0
  56. package/dist/api/route-helpers.js.map +1 -1
  57. package/dist/api/routes/ai-seo.d.ts.map +1 -1
  58. package/dist/api/routes/ai-seo.js +32 -1
  59. package/dist/api/routes/ai-seo.js.map +1 -1
  60. package/dist/api/routes/documents.d.ts.map +1 -1
  61. package/dist/api/routes/documents.js +3 -3
  62. package/dist/api/routes/documents.js.map +1 -1
  63. package/dist/api/routes/seo-public.d.ts.map +1 -1
  64. package/dist/api/routes/seo-public.js +371 -62
  65. package/dist/api/routes/seo-public.js.map +1 -1
  66. package/dist/api/routes/seo.d.ts.map +1 -1
  67. package/dist/api/routes/seo.js +124 -16
  68. package/dist/api/routes/seo.js.map +1 -1
  69. package/dist/api/routes/url-resolution.d.ts.map +1 -1
  70. package/dist/api/routes/url-resolution.js +40 -1
  71. package/dist/api/routes/url-resolution.js.map +1 -1
  72. package/dist/client.d.ts +1 -0
  73. package/dist/client.d.ts.map +1 -1
  74. package/dist/client.js +3 -0
  75. package/dist/client.js.map +1 -1
  76. package/dist/config/types.d.ts +51 -1
  77. package/dist/config/types.d.ts.map +1 -1
  78. package/dist/content/index.d.ts +1 -0
  79. package/dist/content/index.d.ts.map +1 -1
  80. package/dist/content/index.js +1 -0
  81. package/dist/content/index.js.map +1 -1
  82. package/dist/content/slug.d.ts +40 -0
  83. package/dist/content/slug.d.ts.map +1 -0
  84. package/dist/content/slug.js +78 -0
  85. package/dist/content/slug.js.map +1 -0
  86. package/dist/content/structured-data.d.ts +6 -0
  87. package/dist/content/structured-data.d.ts.map +1 -1
  88. package/dist/content/structured-data.js +6 -0
  89. package/dist/content/structured-data.js.map +1 -1
  90. package/dist/content-health/scanner.d.ts +4 -3
  91. package/dist/content-health/scanner.d.ts.map +1 -1
  92. package/dist/content-health/scanner.js +6 -12
  93. package/dist/content-health/scanner.js.map +1 -1
  94. package/dist/redirects/auto.d.ts +45 -0
  95. package/dist/redirects/auto.d.ts.map +1 -0
  96. package/dist/redirects/auto.js +55 -0
  97. package/dist/redirects/auto.js.map +1 -0
  98. package/dist/redirects/index.d.ts +2 -0
  99. package/dist/redirects/index.d.ts.map +1 -1
  100. package/dist/redirects/index.js +1 -0
  101. package/dist/redirects/index.js.map +1 -1
  102. package/dist/seo/ai-bots.d.ts +42 -0
  103. package/dist/seo/ai-bots.d.ts.map +1 -0
  104. package/dist/seo/ai-bots.js +104 -0
  105. package/dist/seo/ai-bots.js.map +1 -0
  106. package/dist/seo/alt-text.d.ts +39 -0
  107. package/dist/seo/alt-text.d.ts.map +1 -0
  108. package/dist/seo/alt-text.js +67 -0
  109. package/dist/seo/alt-text.js.map +1 -0
  110. package/dist/seo/analysis.d.ts.map +1 -1
  111. package/dist/seo/analysis.js +7 -11
  112. package/dist/seo/analysis.js.map +1 -1
  113. package/dist/seo/audit-engine.d.ts +1 -0
  114. package/dist/seo/audit-engine.d.ts.map +1 -1
  115. package/dist/seo/audit-engine.js +4 -4
  116. package/dist/seo/audit-engine.js.map +1 -1
  117. package/dist/seo/audit-runner.d.ts.map +1 -1
  118. package/dist/seo/audit-runner.js +1 -0
  119. package/dist/seo/audit-runner.js.map +1 -1
  120. package/dist/seo/feeds.d.ts +36 -0
  121. package/dist/seo/feeds.d.ts.map +1 -0
  122. package/dist/seo/feeds.js +71 -0
  123. package/dist/seo/feeds.js.map +1 -0
  124. package/dist/seo/index.d.ts +10 -4
  125. package/dist/seo/index.d.ts.map +1 -1
  126. package/dist/seo/index.js +6 -3
  127. package/dist/seo/index.js.map +1 -1
  128. package/dist/seo/llms-txt.d.ts +24 -0
  129. package/dist/seo/llms-txt.d.ts.map +1 -1
  130. package/dist/seo/llms-txt.js +109 -47
  131. package/dist/seo/llms-txt.js.map +1 -1
  132. package/dist/seo/meta-tags.d.ts +14 -0
  133. package/dist/seo/meta-tags.d.ts.map +1 -1
  134. package/dist/seo/meta-tags.js +21 -2
  135. package/dist/seo/meta-tags.js.map +1 -1
  136. package/dist/seo/page-meta.d.ts +47 -0
  137. package/dist/seo/page-meta.d.ts.map +1 -1
  138. package/dist/seo/page-meta.js +280 -70
  139. package/dist/seo/page-meta.js.map +1 -1
  140. package/dist/seo/sitemap-notify.d.ts +30 -0
  141. package/dist/seo/sitemap-notify.d.ts.map +1 -1
  142. package/dist/seo/sitemap-notify.js +87 -0
  143. package/dist/seo/sitemap-notify.js.map +1 -1
  144. package/dist/validation/publish.d.ts +53 -0
  145. package/dist/validation/publish.d.ts.map +1 -0
  146. package/dist/validation/publish.js +75 -0
  147. package/dist/validation/publish.js.map +1 -0
  148. package/package.json +1 -1
@@ -31,6 +31,7 @@
31
31
  * collapse to one row per (document, type) with `count` carrying
32
32
  * the per-document magnitude.
33
33
  */
34
+ import { countMissingAltImages as sharedCountMissingAltImages } from '../seo/alt-text.js';
34
35
  // ─── Helpers ───────────────────────────────────────────────────────────────
35
36
  /**
36
37
  * Return the longest plausible HTML payload from a document. CMS
@@ -67,20 +68,13 @@ function getMetaDescription(data) {
67
68
  return null;
68
69
  }
69
70
  /**
70
- * Count <img> tags in the HTML that don't carry an `alt` attribute.
71
- * Matches the existing a11y rule in `a11y/index.ts` so the scanner
72
- * agrees with the per-document audit panel.
71
+ * Count <img> tags in the HTML that are missing alt text, excluding images
72
+ * correctly marked decorative (empty alt + role="presentation"/aria-hidden).
73
+ * Delegates to the shared, decorative-aware analyzer so every SEO surface
74
+ * (analysis, audit engine, this scanner, the cron scan) agrees.
73
75
  */
74
76
  export function countMissingAltImages(html) {
75
- if (!html)
76
- return 0;
77
- const re = /<img(?![^>]*\salt\s*=)[^>]*>/gi;
78
- let count = 0;
79
- // We don't iterate matches into an array — just count, so memory
80
- // stays bounded even on huge pages.
81
- while (re.exec(html) !== null)
82
- count++;
83
- return count;
77
+ return sharedCountMissingAltImages(html);
84
78
  }
85
79
  /**
86
80
  * Extract candidate URLs from <a href="..."> attributes. Returns
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../src/content-health/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAmDH,8EAA8E;AAE9E;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,IAA6B;IAChD,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAA;IACrF,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;YAAE,IAAI,GAAG,CAAC,CAAA;IAC/D,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,CAAC,CAAA;AAEvF,SAAS,kBAAkB,CAAC,IAA6B;IACvD,KAAK,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IACnE,CAAC;IACD,kDAAkD;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,GAA8B,CAAA;QAC1C,MAAM,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,iBAAiB,CAAC,CAAA;QACtD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IACnE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAA;IACnB,MAAM,EAAE,GAAG,gCAAgC,CAAA;IAC3C,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,iEAAiE;IACjE,oCAAoC;IACpC,OAAO,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI;QAAE,KAAK,EAAE,CAAA;IACtC,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAA;IACpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,MAAM,EAAE,GAAG,+CAA+C,CAAA;IAC1D,IAAI,KAA6B,CAAA;IACjC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC/C,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,UAAkB;IAElB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IACtE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IACzE,oCAAoC;IACpC,MAAM,WAAW,GAAG,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC1D,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAA;QAC5C,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC3F,CAAC;IAED,IAAI,YAAiB,CAAA;IACrB,IAAI,CAAC;QACH,YAAY,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC9C,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QAC/C,IAAI,QAAQ,CAAC,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACpE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QAC9C,CAAC;QACD,OAAO;YACL,UAAU,EAAE,QAAQ,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM;YACnD,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE;SAC9B,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC9C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,GAAW,EAAE,SAAS,GAAG,IAAI;IAC5E,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAA;IACzD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAA;QACF,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC/C,yDAAyD;YACzD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,KAAK;gBACb,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;YACF,OAAO,GAAG,CAAC,MAAM,GAAG,GAAG,CAAA;QACzB,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC;AAED,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAiB,EACjB,OAAuB,EAAE;IAEzB,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAmB,EAAE,CAAA;IAEjC,2EAA2E;IAC3E,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IAC7D,CAAC;IAED,2EAA2E;IAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,aAAa,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACjD,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;IACjE,CAAC;IAED,2EAA2E;IAC3E,MAAM,aAAa,GAAG,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,SAAS,CAAA;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,OAAO,EAAE,CAAA;IAClD,IAAI,KAAK,GAAG,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,kBAAkB;YACxB,KAAK,EAAE,CAAC;YACR,OAAO,EAAE;gBACP,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;gBAClD,SAAS,EAAE,YAAY;aACxB;SACF,CAAC,CAAA;IACJ,CAAC;IAED,0EAA0E;IAC1E,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;QAChC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAA;QACtC,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YACnE,IAAI,UAAU,IAAI,QAAQ;gBAAE,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACxD,CAAC;QACD,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,IAAI,YAAY,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAA;YAC9B,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,CAAC,GAAG,YAAY,CAAC,EAAE,sBAAsB,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBACnE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;gBAClF,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;oBACrB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;wBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;gBACjC,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CACH,CAAA;QACH,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,sBAAsB;gBAC5B,KAAK,EAAE,MAAM,CAAC,MAAM;gBACpB,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,qBAAqB;aAC9D,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEhC,SAAS,KAAK,CAAI,GAAQ,EAAE,IAAY;IACtC,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,CAAC,GAAG,CAAC,CAAA;IAC3B,MAAM,GAAG,GAAU,EAAE,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;IAC3E,OAAO,GAAG,CAAA;AACZ,CAAC"}
1
+ {"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../src/content-health/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,qBAAqB,IAAI,2BAA2B,EAAE,MAAM,oBAAoB,CAAA;AAmDzF,8EAA8E;AAE9E;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,IAA6B;IAChD,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAA;IACrF,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;YAAE,IAAI,GAAG,CAAC,CAAA;IAC/D,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,CAAC,CAAA;AAEvF,SAAS,kBAAkB,CAAC,IAA6B;IACvD,KAAK,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IACnE,CAAC;IACD,kDAAkD;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,GAA8B,CAAA;QAC1C,MAAM,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,iBAAiB,CAAC,CAAA;QACtD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IACnE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAA;IACpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,MAAM,EAAE,GAAG,+CAA+C,CAAA;IAC1D,IAAI,KAA6B,CAAA;IACjC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC/C,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,UAAkB;IAElB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IACtE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IACzE,oCAAoC;IACpC,MAAM,WAAW,GAAG,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC1D,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAA;QAC5C,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC3F,CAAC;IAED,IAAI,YAAiB,CAAA;IACrB,IAAI,CAAC;QACH,YAAY,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC9C,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QAC/C,IAAI,QAAQ,CAAC,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACpE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QAC9C,CAAC;QACD,OAAO;YACL,UAAU,EAAE,QAAQ,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM;YACnD,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE;SAC9B,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC9C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,GAAW,EAAE,SAAS,GAAG,IAAI;IAC5E,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAA;IACzD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAA;QACF,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC/C,yDAAyD;YACzD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,KAAK;gBACb,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;YACF,OAAO,GAAG,CAAC,MAAM,GAAG,GAAG,CAAA;QACzB,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC;AAED,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAiB,EACjB,OAAuB,EAAE;IAEzB,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAmB,EAAE,CAAA;IAEjC,2EAA2E;IAC3E,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IAC7D,CAAC;IAED,2EAA2E;IAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,aAAa,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACjD,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;IACjE,CAAC;IAED,2EAA2E;IAC3E,MAAM,aAAa,GAAG,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,SAAS,CAAA;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,OAAO,EAAE,CAAA;IAClD,IAAI,KAAK,GAAG,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,kBAAkB;YACxB,KAAK,EAAE,CAAC;YACR,OAAO,EAAE;gBACP,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;gBAClD,SAAS,EAAE,YAAY;aACxB;SACF,CAAC,CAAA;IACJ,CAAC;IAED,0EAA0E;IAC1E,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;QAChC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAA;QACtC,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YACnE,IAAI,UAAU,IAAI,QAAQ;gBAAE,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACxD,CAAC;QACD,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,IAAI,YAAY,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAA;YAC9B,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,CAAC,GAAG,YAAY,CAAC,EAAE,sBAAsB,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBACnE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;gBAClF,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;oBACrB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;wBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;gBACjC,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CACH,CAAA;QACH,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,sBAAsB;gBAC5B,KAAK,EAAE,MAAM,CAAC,MAAM;gBACpB,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,qBAAqB;aAC9D,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEhC,SAAS,KAAK,CAAI,GAAQ,EAAE,IAAY;IACtC,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,CAAC,GAAG,CAAC,CAAA;IAC3B,MAAM,GAAG,GAAU,EAAE,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;IAC3E,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Auto-301 planning for slug/path changes.
3
+ *
4
+ * When a *published* document's resolved URL changes, the old URL was already
5
+ * crawled/linked/indexed and must not 404. This module computes — as a pure,
6
+ * fully testable function — the redirect mutations needed to keep the old URL
7
+ * alive without building chains or loops:
8
+ *
9
+ * 1. Drop any rule whose source is the new (now-live) path, which would
10
+ * otherwise redirect the page away from itself.
11
+ * 2. Repoint any rule that pointed at the old path so it points at the new
12
+ * path directly (chain flattening — never old→mid→new).
13
+ * 3. Add old→new, but only if the simulated post-mutation rule set stays
14
+ * loop-free (e.g. a pre-existing new→old rule we are not removing).
15
+ *
16
+ * The caller (actions.ts) applies the returned plan in a single transaction.
17
+ */
18
+ import { type RedirectRecord } from './graph.js';
19
+ export interface SlugRedirectPlan {
20
+ /** Redirect to create or upsert (source already normalized), or null to skip. */
21
+ upsert: {
22
+ source: string;
23
+ destination: string;
24
+ } | null;
25
+ /** Existing redirect ids to delete (their source is the now-live new path). */
26
+ deleteIds: string[];
27
+ /** Existing redirects to repoint at the new path (chain flattening). */
28
+ repoint: Array<{
29
+ id: string;
30
+ destination: string;
31
+ }>;
32
+ }
33
+ /** True when the plan would make no database changes. */
34
+ export declare function isEmptyPlan(plan: SlugRedirectPlan): boolean;
35
+ /**
36
+ * Compute the redirect mutations for a slug/path change. Both paths are
37
+ * site-relative (e.g. `/old-about`); normalization (leading slash, no trailing
38
+ * slash, lowercase) is applied internally and matches request-time matching.
39
+ */
40
+ export declare function planSlugChangeRedirects(params: {
41
+ oldPath: string;
42
+ newPath: string;
43
+ existing: RedirectRecord[];
44
+ }): SlugRedirectPlan;
45
+ //# sourceMappingURL=auto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto.d.ts","sourceRoot":"","sources":["../../src/redirects/auto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAA0C,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAExF,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACtD,+EAA+E;IAC/E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,wEAAwE;IACxE,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACpD;AAID,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,cAAc,EAAE,CAAA;CAC3B,GAAG,gBAAgB,CAiCnB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Auto-301 planning for slug/path changes.
3
+ *
4
+ * When a *published* document's resolved URL changes, the old URL was already
5
+ * crawled/linked/indexed and must not 404. This module computes — as a pure,
6
+ * fully testable function — the redirect mutations needed to keep the old URL
7
+ * alive without building chains or loops:
8
+ *
9
+ * 1. Drop any rule whose source is the new (now-live) path, which would
10
+ * otherwise redirect the page away from itself.
11
+ * 2. Repoint any rule that pointed at the old path so it points at the new
12
+ * path directly (chain flattening — never old→mid→new).
13
+ * 3. Add old→new, but only if the simulated post-mutation rule set stays
14
+ * loop-free (e.g. a pre-existing new→old rule we are not removing).
15
+ *
16
+ * The caller (actions.ts) applies the returned plan in a single transaction.
17
+ */
18
+ import { normalizeRedirectPath, wouldCreateLoop } from './graph.js';
19
+ const EMPTY_PLAN = { upsert: null, deleteIds: [], repoint: [] };
20
+ /** True when the plan would make no database changes. */
21
+ export function isEmptyPlan(plan) {
22
+ return !plan.upsert && plan.deleteIds.length === 0 && plan.repoint.length === 0;
23
+ }
24
+ /**
25
+ * Compute the redirect mutations for a slug/path change. Both paths are
26
+ * site-relative (e.g. `/old-about`); normalization (leading slash, no trailing
27
+ * slash, lowercase) is applied internally and matches request-time matching.
28
+ */
29
+ export function planSlugChangeRedirects(params) {
30
+ const oldN = normalizeRedirectPath(params.oldPath);
31
+ const newN = normalizeRedirectPath(params.newPath);
32
+ // Nothing to do when the URL did not actually change. Never redirect the
33
+ // site root onto itself or onto a child — a homepage "move" is almost always
34
+ // an unrelated path edit and a `/` → /x rule would break the whole site.
35
+ if (!oldN || !newN || oldN === newN || oldN === '/')
36
+ return EMPTY_PLAN;
37
+ const deleteIds = params.existing
38
+ .filter((r) => normalizeRedirectPath(r.from) === newN)
39
+ .map((r) => r.id);
40
+ const repoint = params.existing
41
+ .filter((r) => !deleteIds.includes(r.id) &&
42
+ normalizeRedirectPath(r.to) === oldN &&
43
+ normalizeRedirectPath(r.from) !== newN)
44
+ .map((r) => ({ id: r.id, destination: newN }));
45
+ // Simulate the post-mutation rule set (deletes applied, repoints applied)
46
+ // and only add the new edge if it stays loop-free.
47
+ const remaining = params.existing
48
+ .filter((r) => !deleteIds.includes(r.id))
49
+ .map((r) => (repoint.some((p) => p.id === r.id) ? { ...r, to: newN } : r));
50
+ if (wouldCreateLoop(remaining, oldN, newN)) {
51
+ return { upsert: null, deleteIds, repoint };
52
+ }
53
+ return { upsert: { source: oldN, destination: newN }, deleteIds, repoint };
54
+ }
55
+ //# sourceMappingURL=auto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto.js","sourceRoot":"","sources":["../../src/redirects/auto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAuB,MAAM,YAAY,CAAA;AAWxF,MAAM,UAAU,GAAqB,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;AAEjF,yDAAyD;AACzD,MAAM,UAAU,WAAW,CAAC,IAAsB;IAChD,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAA;AACjF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAIvC;IACC,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAClD,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAElD,yEAAyE;IACzE,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,UAAU,CAAA;IAEtE,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ;SAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;SACrD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAEnB,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ;SAC5B,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI;QACpC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CACzC;SACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAEhD,0EAA0E;IAC1E,mDAAmD;IACnD,MAAM,SAAS,GAAqB,MAAM,CAAC,QAAQ;SAChD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SACxC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAE5E,IAAI,eAAe,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAA;IAC7C,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAA;AAC5E,CAAC"}
@@ -2,4 +2,6 @@ export { normalizeRedirectPath, isExternal, buildEdgeMap, resolveChain, detectCh
2
2
  export type { RedirectRecord, ChainResolution, DetectedChain } from './graph.js';
3
3
  export { suggestRedirectTarget } from './suggest.js';
4
4
  export type { SuggestCandidate, RedirectSuggestionResult } from './suggest.js';
5
+ export { planSlugChangeRedirects, isEmptyPlan } from './auto.js';
6
+ export type { SlugRedirectPlan } from './auto.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/redirects/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEpD,YAAY,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/redirects/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEpD,YAAY,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAE9E,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAEhE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA"}
@@ -1,3 +1,4 @@
1
1
  export { normalizeRedirectPath, isExternal, buildEdgeMap, resolveChain, detectChainsAndLoops, wouldCreateLoop, isRedirectStale, } from './graph.js';
2
2
  export { suggestRedirectTarget } from './suggest.js';
3
+ export { planSlugChangeRedirects, isEmptyPlan } from './auto.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/redirects/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AAInB,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/redirects/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AAInB,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAIpD,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AI crawler taxonomy + robots.txt policy.
3
+ *
4
+ * Not all "AI bots" are equal, and blocking them all is usually the wrong call
5
+ * for visibility:
6
+ *
7
+ * - TRAINING crawlers ingest your content into model weights and never send a
8
+ * user or a citation back. Blocking these has little downside for traffic
9
+ * and is what most "block AI" intentions actually mean.
10
+ * - RETRIEVAL / answer-engine crawlers fetch a page to answer a user's live
11
+ * query and typically cite + link the source (ChatGPT browsing, Perplexity,
12
+ * etc.). Blocking these removes you from AI answers — usually undesirable.
13
+ *
14
+ * So we model the two independently. Lists are curated for 2026 and grouped by
15
+ * vendor for maintainability; keep them conservative (only well-attested UA
16
+ * tokens) to avoid accidentally blocking a general search crawler.
17
+ */
18
+ /** Crawlers that ingest content for model TRAINING (no citation, no referral). */
19
+ export declare const AI_TRAINING_BOTS: readonly string[];
20
+ /** Answer-engine / live-retrieval crawlers (cite + refer users back). */
21
+ export declare const AI_RETRIEVAL_BOTS: readonly string[];
22
+ export type AiBotStance = 'allow' | 'block';
23
+ export interface AiBotPolicy {
24
+ /** Stance toward TRAINING crawlers. Default `allow`. */
25
+ training?: AiBotStance;
26
+ /** Stance toward RETRIEVAL / answer-engine crawlers. Default `allow`. */
27
+ retrieval?: AiBotStance;
28
+ }
29
+ /**
30
+ * Build the robots.txt User-agent/Disallow line groups for blocked AI bots.
31
+ *
32
+ * Precedence:
33
+ * 1. An explicit `policy` (nuanced training vs retrieval) wins.
34
+ * 2. Otherwise the legacy `blockAIBots` boolean blocks BOTH groups (preserves
35
+ * pre-nuance behavior for sites that set it).
36
+ * 3. Otherwise nothing is emitted.
37
+ *
38
+ * Returns a flat array of robots.txt lines (each blocked bot gets its own
39
+ * `User-agent` + `Disallow: /` + blank-line group). Pure + deterministic.
40
+ */
41
+ export declare function buildAiBotRobotsLines(policy: AiBotPolicy | undefined, legacyBlockAIBots: boolean | undefined): string[];
42
+ //# sourceMappingURL=ai-bots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-bots.d.ts","sourceRoot":"","sources":["../../src/seo/ai-bots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EA0B7C,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAiB9C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,CAAA;AAE3C,MAAM,WAAW,WAAW;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,yEAAyE;IACzE,SAAS,CAAC,EAAE,WAAW,CAAA;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,iBAAiB,EAAE,OAAO,GAAG,SAAS,GACrC,MAAM,EAAE,CAyBV"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * AI crawler taxonomy + robots.txt policy.
3
+ *
4
+ * Not all "AI bots" are equal, and blocking them all is usually the wrong call
5
+ * for visibility:
6
+ *
7
+ * - TRAINING crawlers ingest your content into model weights and never send a
8
+ * user or a citation back. Blocking these has little downside for traffic
9
+ * and is what most "block AI" intentions actually mean.
10
+ * - RETRIEVAL / answer-engine crawlers fetch a page to answer a user's live
11
+ * query and typically cite + link the source (ChatGPT browsing, Perplexity,
12
+ * etc.). Blocking these removes you from AI answers — usually undesirable.
13
+ *
14
+ * So we model the two independently. Lists are curated for 2026 and grouped by
15
+ * vendor for maintainability; keep them conservative (only well-attested UA
16
+ * tokens) to avoid accidentally blocking a general search crawler.
17
+ */
18
+ /** Crawlers that ingest content for model TRAINING (no citation, no referral). */
19
+ export const AI_TRAINING_BOTS = [
20
+ // OpenAI
21
+ 'GPTBot',
22
+ // Anthropic
23
+ 'ClaudeBot',
24
+ 'anthropic-ai',
25
+ // Google (Gemini training; distinct from Googlebot search)
26
+ 'Google-Extended',
27
+ // Common Crawl (feeds many training sets)
28
+ 'CCBot',
29
+ // ByteDance
30
+ 'Bytespider',
31
+ // Apple (Apple Intelligence training; distinct from Applebot search)
32
+ 'Applebot-Extended',
33
+ // Meta
34
+ 'Meta-ExternalAgent',
35
+ 'FacebookBot',
36
+ // Misc training/scraping crawlers
37
+ 'Diffbot',
38
+ 'Omgilibot',
39
+ 'omgili',
40
+ 'PanguBot',
41
+ 'Timpibot',
42
+ 'cohere-ai',
43
+ 'ImagesiftBot',
44
+ 'PetalBot',
45
+ ];
46
+ /** Answer-engine / live-retrieval crawlers (cite + refer users back). */
47
+ export const AI_RETRIEVAL_BOTS = [
48
+ // OpenAI
49
+ 'OAI-SearchBot',
50
+ 'ChatGPT-User',
51
+ // Perplexity
52
+ 'PerplexityBot',
53
+ 'Perplexity-User',
54
+ // Anthropic retrieval
55
+ 'Claude-Web',
56
+ 'Claude-User',
57
+ // Apple search (Siri/Spotlight)
58
+ 'Applebot',
59
+ // Amazon (Alexa)
60
+ 'Amazonbot',
61
+ // You.com / DuckDuckGo assist
62
+ 'YouBot',
63
+ 'DuckAssistBot',
64
+ ];
65
+ /**
66
+ * Build the robots.txt User-agent/Disallow line groups for blocked AI bots.
67
+ *
68
+ * Precedence:
69
+ * 1. An explicit `policy` (nuanced training vs retrieval) wins.
70
+ * 2. Otherwise the legacy `blockAIBots` boolean blocks BOTH groups (preserves
71
+ * pre-nuance behavior for sites that set it).
72
+ * 3. Otherwise nothing is emitted.
73
+ *
74
+ * Returns a flat array of robots.txt lines (each blocked bot gets its own
75
+ * `User-agent` + `Disallow: /` + blank-line group). Pure + deterministic.
76
+ */
77
+ export function buildAiBotRobotsLines(policy, legacyBlockAIBots) {
78
+ let blockTraining;
79
+ let blockRetrieval;
80
+ if (policy && (policy.training || policy.retrieval)) {
81
+ blockTraining = policy.training === 'block';
82
+ blockRetrieval = policy.retrieval === 'block';
83
+ }
84
+ else if (legacyBlockAIBots) {
85
+ blockTraining = true;
86
+ blockRetrieval = true;
87
+ }
88
+ else {
89
+ return [];
90
+ }
91
+ const bots = [];
92
+ if (blockTraining)
93
+ bots.push(...AI_TRAINING_BOTS);
94
+ if (blockRetrieval)
95
+ bots.push(...AI_RETRIEVAL_BOTS);
96
+ const lines = [];
97
+ for (const bot of bots) {
98
+ lines.push(`User-agent: ${bot}`);
99
+ lines.push('Disallow: /');
100
+ lines.push('');
101
+ }
102
+ return lines;
103
+ }
104
+ //# sourceMappingURL=ai-bots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-bots.js","sourceRoot":"","sources":["../../src/seo/ai-bots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,WAAW;IACX,cAAc;IACd,2DAA2D;IAC3D,iBAAiB;IACjB,0CAA0C;IAC1C,OAAO;IACP,YAAY;IACZ,YAAY;IACZ,qEAAqE;IACrE,mBAAmB;IACnB,OAAO;IACP,oBAAoB;IACpB,aAAa;IACb,kCAAkC;IAClC,SAAS;IACT,WAAW;IACX,QAAQ;IACR,UAAU;IACV,UAAU;IACV,WAAW;IACX,cAAc;IACd,UAAU;CACX,CAAA;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,SAAS;IACT,eAAe;IACf,cAAc;IACd,aAAa;IACb,eAAe;IACf,iBAAiB;IACjB,sBAAsB;IACtB,YAAY;IACZ,aAAa;IACb,gCAAgC;IAChC,UAAU;IACV,iBAAiB;IACjB,WAAW;IACX,8BAA8B;IAC9B,QAAQ;IACR,eAAe;CAChB,CAAA;AAWD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAA+B,EAC/B,iBAAsC;IAEtC,IAAI,aAAsB,CAAA;IAC1B,IAAI,cAAuB,CAAA;IAE3B,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACpD,aAAa,GAAG,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAA;QAC3C,cAAc,GAAG,MAAM,CAAC,SAAS,KAAK,OAAO,CAAA;IAC/C,CAAC;SAAM,IAAI,iBAAiB,EAAE,CAAC;QAC7B,aAAa,GAAG,IAAI,CAAA;QACpB,cAAc,GAAG,IAAI,CAAA;IACvB,CAAC;SAAM,CAAC;QACN,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,IAAI,GAAa,EAAE,CAAA;IACzB,IAAI,aAAa;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAA;IACjD,IAAI,cAAc;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAA;IAEnD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC,CAAA;QAChC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACzB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChB,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Canonical image alt-text analysis, shared by every surface that reports on
3
+ * alt coverage (SEO analysis, the audit engine, the content-health scanner, the
4
+ * cron scan). Previously each had its own regex and they disagreed on the most
5
+ * important edge case — an intentionally *decorative* image — which produced
6
+ * conflicting "missing alt" counts.
7
+ *
8
+ * Rules (chosen to match the long-standing scanner/cron contract: an explicit
9
+ * empty `alt=""` is an intentional, valid decorative choice — only a *missing*
10
+ * `alt` attribute is an error):
11
+ * - DECORATIVE: an empty `alt=""` (the author opted the image out of the
12
+ * accessibility tree), or any image explicitly marked decorative for
13
+ * assistive tech (`role="presentation"`, `role="none"`, `aria-hidden="true"`).
14
+ * Decorative images are correct and never flagged as missing.
15
+ * - MISSING: a non-decorative image with no `alt` attribute at all.
16
+ * - HAS ALT: a non-empty `alt`.
17
+ */
18
+ /**
19
+ * True when an `<img>` tag is decorative: either an explicit empty `alt=""`, or
20
+ * marked decorative for assistive tech (`role="presentation"`/`"none"` or
21
+ * `aria-hidden="true"`). A descriptive (non-empty) alt is never decorative.
22
+ */
23
+ export declare function isDecorativeImgTag(tag: string): boolean;
24
+ export interface ImageAltAnalysis {
25
+ /** All `<img>` tags found. */
26
+ total: number;
27
+ /** Non-decorative images that have non-empty alt text. */
28
+ withAlt: number;
29
+ /** Images correctly marked decorative (excluded from coverage math). */
30
+ decorative: number;
31
+ /** Non-decorative images with no/empty alt. */
32
+ missing: number;
33
+ /** The non-empty alt strings, trimmed. */
34
+ altTexts: string[];
35
+ }
36
+ export declare function analyzeImageAlts(html: string | null | undefined): ImageAltAnalysis;
37
+ /** Count images missing alt text (decorative images excluded). */
38
+ export declare function countMissingAltImages(html: string | null | undefined): number;
39
+ //# sourceMappingURL=alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alt-text.d.ts","sourceRoot":"","sources":["../../src/seo/alt-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAYH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,gBAAgB,CAiBlF;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE7E"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Canonical image alt-text analysis, shared by every surface that reports on
3
+ * alt coverage (SEO analysis, the audit engine, the content-health scanner, the
4
+ * cron scan). Previously each had its own regex and they disagreed on the most
5
+ * important edge case — an intentionally *decorative* image — which produced
6
+ * conflicting "missing alt" counts.
7
+ *
8
+ * Rules (chosen to match the long-standing scanner/cron contract: an explicit
9
+ * empty `alt=""` is an intentional, valid decorative choice — only a *missing*
10
+ * `alt` attribute is an error):
11
+ * - DECORATIVE: an empty `alt=""` (the author opted the image out of the
12
+ * accessibility tree), or any image explicitly marked decorative for
13
+ * assistive tech (`role="presentation"`, `role="none"`, `aria-hidden="true"`).
14
+ * Decorative images are correct and never flagged as missing.
15
+ * - MISSING: a non-decorative image with no `alt` attribute at all.
16
+ * - HAS ALT: a non-empty `alt`.
17
+ */
18
+ const IMG_TAG_RE = /<img\b[^>]*>/gi;
19
+ function getAttr(tag, name) {
20
+ // The negative lookbehind prevents matching a hyphenated/namespaced attribute
21
+ // that merely *ends* with `name` (e.g. `data-default-alt="…"` must not be read
22
+ // as the `alt` attribute).
23
+ const m = tag.match(new RegExp(`(?<![-\\w:])${name}\\s*=\\s*["']([^"']*)["']`, 'i'));
24
+ return m ? (m[1] ?? '') : null;
25
+ }
26
+ /**
27
+ * True when an `<img>` tag is decorative: either an explicit empty `alt=""`, or
28
+ * marked decorative for assistive tech (`role="presentation"`/`"none"` or
29
+ * `aria-hidden="true"`). A descriptive (non-empty) alt is never decorative.
30
+ */
31
+ export function isDecorativeImgTag(tag) {
32
+ const alt = getAttr(tag, 'alt');
33
+ if (alt !== null && alt.trim().length > 0)
34
+ return false;
35
+ if (alt !== null && alt.trim().length === 0)
36
+ return true;
37
+ // No alt attribute at all: decorative only if explicitly marked so.
38
+ const role = getAttr(tag, 'role')?.toLowerCase();
39
+ const ariaHidden = getAttr(tag, 'aria-hidden')?.toLowerCase();
40
+ return role === 'presentation' || role === 'none' || ariaHidden === 'true';
41
+ }
42
+ export function analyzeImageAlts(html) {
43
+ const result = { total: 0, withAlt: 0, decorative: 0, missing: 0, altTexts: [] };
44
+ if (!html)
45
+ return result;
46
+ const tags = html.match(IMG_TAG_RE) ?? [];
47
+ result.total = tags.length;
48
+ for (const tag of tags) {
49
+ const alt = getAttr(tag, 'alt');
50
+ if (alt !== null && alt.trim().length > 0) {
51
+ result.withAlt++;
52
+ result.altTexts.push(alt.trim());
53
+ }
54
+ else if (isDecorativeImgTag(tag)) {
55
+ result.decorative++;
56
+ }
57
+ else {
58
+ result.missing++;
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ /** Count images missing alt text (decorative images excluded). */
64
+ export function countMissingAltImages(html) {
65
+ return analyzeImageAlts(html).missing;
66
+ }
67
+ //# sourceMappingURL=alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alt-text.js","sourceRoot":"","sources":["../../src/seo/alt-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,UAAU,GAAG,gBAAgB,CAAA;AAEnC,SAAS,OAAO,CAAC,GAAW,EAAE,IAAY;IACxC,8EAA8E;IAC9E,+EAA+E;IAC/E,2BAA2B;IAC3B,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,eAAe,IAAI,2BAA2B,EAAE,GAAG,CAAC,CAAC,CAAA;IACpF,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC/B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACxD,oEAAoE;IACpE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,CAAA;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,WAAW,EAAE,CAAA;IAC7D,OAAO,IAAI,KAAK,cAAc,IAAI,IAAI,KAAK,MAAM,IAAI,UAAU,KAAK,MAAM,CAAA;AAC5E,CAAC;AAeD,MAAM,UAAU,gBAAgB,CAAC,IAA+B;IAC9D,MAAM,MAAM,GAAqB,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;IAClG,IAAI,CAAC,IAAI;QAAE,OAAO,MAAM,CAAA;IACxB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;IACzC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAA;IAC1B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC/B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,CAAC,OAAO,EAAE,CAAA;YAChB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;QAClC,CAAC;aAAM,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,MAAM,CAAC,UAAU,EAAE,CAAA;QACrB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,OAAO,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,qBAAqB,CAAC,IAA+B;IACnE,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAA;AACvC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/seo/analysis.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,QAAQ,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,WAAW,GAAG,aAAa,GAAG,KAAK,GAAG,QAAQ,CAAA;IACxD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAC1C,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AA8DD,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBnD;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAED,uCAAuC;AACvC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrD;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKtD;AA+BD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAiC/E;AAgED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,iBAAiB,CAsZtE;AAID;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAsCpE"}
1
+ {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/seo/analysis.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,QAAQ,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,WAAW,GAAG,aAAa,GAAG,KAAK,GAAG,QAAQ,CAAA;IACxD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAC1C,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AA8DD,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBnD;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAED,uCAAuC;AACvC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrD;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKtD;AA+BD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAiC/E;AA2DD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,iBAAiB,CAsZtE;AAID;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAsCpE"}
@@ -2,6 +2,7 @@
2
2
  // SEO Content Analysis Engine
3
3
  // Pure TypeScript — no external dependencies
4
4
  // ---------------------------------------------------------------------------
5
+ import { analyzeImageAlts } from './alt-text.js';
5
6
  // ---- Helpers --------------------------------------------------------------
6
7
  const TRANSITION_WORDS = [
7
8
  'however',
@@ -197,17 +198,12 @@ export function extractHeadings(content, level) {
197
198
  return headings;
198
199
  }
199
200
  function extractImgAlts(html) {
200
- const imgs = html.match(/<img[^>]*>/gi) ?? [];
201
- const altTexts = [];
202
- let withAlt = 0;
203
- for (const img of imgs) {
204
- const altMatch = img.match(/alt\s*=\s*["']([^"']*)["']/i);
205
- if (altMatch && altMatch[1] && altMatch[1].trim().length > 0) {
206
- withAlt++;
207
- altTexts.push(altMatch[1].trim());
208
- }
209
- }
210
- return { total: imgs.length, withAlt, altTexts };
201
+ // Decorative images (alt="" + role="presentation"/aria-hidden) are correct,
202
+ // so they're excluded from the coverage denominator: `total` counts only
203
+ // images that are expected to have alt text. `total - withAlt` is the number
204
+ // missing. Shared with the audit engine / scanner so all surfaces agree.
205
+ const info = analyzeImageAlts(html);
206
+ return { total: info.withAlt + info.missing, withAlt: info.withAlt, altTexts: info.altTexts };
211
207
  }
212
208
  function extractLinks(html) {
213
209
  const anchors = html.match(/<a[^>]*href\s*=\s*["'][^"']*["'][^>]*>/gi) ?? [];