mcp-wordpress 2.6.3 → 2.7.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 (333) hide show
  1. package/README.md +1 -1
  2. package/dist/cache/CacheInvalidation.d.ts +25 -6
  3. package/dist/cache/CacheInvalidation.d.ts.map +1 -1
  4. package/dist/cache/CacheInvalidation.js +168 -16
  5. package/dist/cache/CacheInvalidation.js.map +1 -1
  6. package/dist/cache/HttpCacheWrapper.d.ts.map +1 -1
  7. package/dist/cache/HttpCacheWrapper.js +3 -4
  8. package/dist/cache/HttpCacheWrapper.js.map +1 -1
  9. package/dist/cache/SEOCacheManager.d.ts +150 -0
  10. package/dist/cache/SEOCacheManager.d.ts.map +1 -0
  11. package/dist/cache/SEOCacheManager.js +275 -0
  12. package/dist/cache/SEOCacheManager.js.map +1 -0
  13. package/dist/client/SEOWordPressClient.d.ts +164 -0
  14. package/dist/client/SEOWordPressClient.d.ts.map +1 -0
  15. package/dist/client/SEOWordPressClient.js +674 -0
  16. package/dist/client/SEOWordPressClient.js.map +1 -0
  17. package/dist/client/api.d.ts.map +1 -1
  18. package/dist/client/api.js +50 -20
  19. package/dist/client/api.js.map +1 -1
  20. package/dist/client/auth.js +19 -19
  21. package/dist/client/auth.js.map +1 -1
  22. package/dist/client/index.d.ts +11 -0
  23. package/dist/client/index.d.ts.map +1 -0
  24. package/dist/client/index.js +14 -0
  25. package/dist/client/index.js.map +1 -0
  26. package/dist/client/managers/AuthManager.d.ts +39 -0
  27. package/dist/client/managers/AuthManager.d.ts.map +1 -0
  28. package/dist/client/managers/AuthManager.js +142 -0
  29. package/dist/client/managers/AuthManager.js.map +1 -0
  30. package/dist/client/managers/AuthenticationManager.d.ts.map +1 -1
  31. package/dist/client/managers/AuthenticationManager.js +10 -9
  32. package/dist/client/managers/AuthenticationManager.js.map +1 -1
  33. package/dist/client/managers/BaseManager.d.ts.map +1 -1
  34. package/dist/client/managers/BaseManager.js +12 -0
  35. package/dist/client/managers/BaseManager.js.map +1 -1
  36. package/dist/client/managers/ComposedAuthenticationManager.d.ts +94 -0
  37. package/dist/client/managers/ComposedAuthenticationManager.d.ts.map +1 -0
  38. package/dist/client/managers/ComposedAuthenticationManager.js +340 -0
  39. package/dist/client/managers/ComposedAuthenticationManager.js.map +1 -0
  40. package/dist/client/managers/ComposedManagerFactory.d.ts +104 -0
  41. package/dist/client/managers/ComposedManagerFactory.d.ts.map +1 -0
  42. package/dist/client/managers/ComposedManagerFactory.js +180 -0
  43. package/dist/client/managers/ComposedManagerFactory.js.map +1 -0
  44. package/dist/client/managers/ComposedRequestManager.d.ts +82 -0
  45. package/dist/client/managers/ComposedRequestManager.d.ts.map +1 -0
  46. package/dist/client/managers/ComposedRequestManager.js +260 -0
  47. package/dist/client/managers/ComposedRequestManager.js.map +1 -0
  48. package/dist/client/managers/JWTAuthImplementation.d.ts +86 -0
  49. package/dist/client/managers/JWTAuthImplementation.d.ts.map +1 -0
  50. package/dist/client/managers/JWTAuthImplementation.js +240 -0
  51. package/dist/client/managers/JWTAuthImplementation.js.map +1 -0
  52. package/dist/client/managers/ManagersIndex.d.ts +6 -0
  53. package/dist/client/managers/ManagersIndex.d.ts.map +1 -0
  54. package/dist/client/managers/ManagersIndex.js +6 -0
  55. package/dist/client/managers/ManagersIndex.js.map +1 -0
  56. package/dist/client/managers/RequestManager.d.ts.map +1 -1
  57. package/dist/client/managers/RequestManager.js +5 -3
  58. package/dist/client/managers/RequestManager.js.map +1 -1
  59. package/dist/client/managers/composed/MigrationAdapter.d.ts +80 -0
  60. package/dist/client/managers/composed/MigrationAdapter.d.ts.map +1 -0
  61. package/dist/client/managers/composed/MigrationAdapter.js +214 -0
  62. package/dist/client/managers/composed/MigrationAdapter.js.map +1 -0
  63. package/dist/client/managers/composed/index.d.ts +23 -0
  64. package/dist/client/managers/composed/index.d.ts.map +1 -0
  65. package/dist/client/managers/composed/index.js +26 -0
  66. package/dist/client/managers/composed/index.js.map +1 -0
  67. package/dist/client/managers/implementations/ConfigurationProviderImpl.d.ts +27 -0
  68. package/dist/client/managers/implementations/ConfigurationProviderImpl.d.ts.map +1 -0
  69. package/dist/client/managers/implementations/ConfigurationProviderImpl.js +41 -0
  70. package/dist/client/managers/implementations/ConfigurationProviderImpl.js.map +1 -0
  71. package/dist/client/managers/implementations/ErrorHandlerImpl.d.ts +31 -0
  72. package/dist/client/managers/implementations/ErrorHandlerImpl.d.ts.map +1 -0
  73. package/dist/client/managers/implementations/ErrorHandlerImpl.js +73 -0
  74. package/dist/client/managers/implementations/ErrorHandlerImpl.js.map +1 -0
  75. package/dist/client/managers/implementations/ParameterValidatorImpl.d.ts +47 -0
  76. package/dist/client/managers/implementations/ParameterValidatorImpl.d.ts.map +1 -0
  77. package/dist/client/managers/implementations/ParameterValidatorImpl.js +141 -0
  78. package/dist/client/managers/implementations/ParameterValidatorImpl.js.map +1 -0
  79. package/dist/client/managers/interfaces/ManagerInterfaces.d.ts +147 -0
  80. package/dist/client/managers/interfaces/ManagerInterfaces.d.ts.map +1 -0
  81. package/dist/client/managers/interfaces/ManagerInterfaces.js +6 -0
  82. package/dist/client/managers/interfaces/ManagerInterfaces.js.map +1 -0
  83. package/dist/config/Config.d.ts +30 -0
  84. package/dist/config/Config.d.ts.map +1 -1
  85. package/dist/config/Config.js +30 -0
  86. package/dist/config/Config.js.map +1 -1
  87. package/dist/config/ConfigurationSchema.d.ts +75 -198
  88. package/dist/config/ConfigurationSchema.d.ts.map +1 -1
  89. package/dist/config/ConfigurationSchema.js +17 -17
  90. package/dist/config/ConfigurationSchema.js.map +1 -1
  91. package/dist/config/ServerConfiguration.d.ts +2 -2
  92. package/dist/config/ServerConfiguration.d.ts.map +1 -1
  93. package/dist/config/ServerConfiguration.js +15 -13
  94. package/dist/config/ServerConfiguration.js.map +1 -1
  95. package/dist/config/index.d.ts +8 -0
  96. package/dist/config/index.d.ts.map +1 -0
  97. package/dist/config/index.js +11 -0
  98. package/dist/config/index.js.map +1 -0
  99. package/dist/docs/DocumentationGenerator.js +2 -2
  100. package/dist/docs/DocumentationGenerator.js.map +1 -1
  101. package/dist/dxt-entry.js +3 -3
  102. package/dist/dxt-entry.js.map +1 -1
  103. package/dist/index.d.ts +1 -0
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +38 -37
  106. package/dist/index.js.map +1 -1
  107. package/dist/performance/MetricsCollector.d.ts.map +1 -1
  108. package/dist/performance/MetricsCollector.js +5 -4
  109. package/dist/performance/MetricsCollector.js.map +1 -1
  110. package/dist/security/AISecurityScanner.js +7 -7
  111. package/dist/security/AISecurityScanner.js.map +1 -1
  112. package/dist/security/AutomatedRemediation.d.ts.map +1 -1
  113. package/dist/security/AutomatedRemediation.js +11 -11
  114. package/dist/security/AutomatedRemediation.js.map +1 -1
  115. package/dist/security/InputValidator.d.ts +50 -126
  116. package/dist/security/InputValidator.d.ts.map +1 -1
  117. package/dist/security/InputValidator.js +9 -9
  118. package/dist/security/InputValidator.js.map +1 -1
  119. package/dist/security/SecurityCIPipeline.d.ts +47 -5
  120. package/dist/security/SecurityCIPipeline.d.ts.map +1 -1
  121. package/dist/security/SecurityCIPipeline.js +390 -49
  122. package/dist/security/SecurityCIPipeline.js.map +1 -1
  123. package/dist/security/SecurityConfigManager.js +10 -10
  124. package/dist/security/SecurityConfigManager.js.map +1 -1
  125. package/dist/security/SecurityMonitoring.js +4 -4
  126. package/dist/security/SecurityMonitoring.js.map +1 -1
  127. package/dist/security/SecurityReviewer.d.ts.map +1 -1
  128. package/dist/security/SecurityReviewer.js +13 -6
  129. package/dist/security/SecurityReviewer.js.map +1 -1
  130. package/dist/security/index.js +3 -3
  131. package/dist/security/index.js.map +1 -1
  132. package/dist/server/ConnectionTester.js +5 -5
  133. package/dist/server/ConnectionTester.js.map +1 -1
  134. package/dist/server/ToolRegistry.d.ts +2 -2
  135. package/dist/server/ToolRegistry.d.ts.map +1 -1
  136. package/dist/server/ToolRegistry.js +7 -6
  137. package/dist/server/ToolRegistry.js.map +1 -1
  138. package/dist/server/index.d.ts +7 -0
  139. package/dist/server/index.d.ts.map +1 -0
  140. package/dist/server/index.js +9 -0
  141. package/dist/server/index.js.map +1 -0
  142. package/dist/tools/BaseToolManager.d.ts.map +1 -1
  143. package/dist/tools/BaseToolManager.js +11 -11
  144. package/dist/tools/BaseToolManager.js.map +1 -1
  145. package/dist/tools/auth.d.ts.map +1 -1
  146. package/dist/tools/auth.js +7 -7
  147. package/dist/tools/auth.js.map +1 -1
  148. package/dist/tools/comments.d.ts.map +1 -1
  149. package/dist/tools/comments.js +14 -14
  150. package/dist/tools/comments.js.map +1 -1
  151. package/dist/tools/index.d.ts +1 -0
  152. package/dist/tools/index.d.ts.map +1 -1
  153. package/dist/tools/index.js +1 -0
  154. package/dist/tools/index.js.map +1 -1
  155. package/dist/tools/media.d.ts.map +1 -1
  156. package/dist/tools/media.js +14 -11
  157. package/dist/tools/media.js.map +1 -1
  158. package/dist/tools/pages.d.ts.map +1 -1
  159. package/dist/tools/pages.js +12 -12
  160. package/dist/tools/pages.js.map +1 -1
  161. package/dist/tools/performance.d.ts.map +1 -1
  162. package/dist/tools/performance.js +13 -11
  163. package/dist/tools/performance.js.map +1 -1
  164. package/dist/tools/posts/PostHandlers.d.ts.map +1 -1
  165. package/dist/tools/posts/PostHandlers.js +16 -16
  166. package/dist/tools/posts/PostHandlers.js.map +1 -1
  167. package/dist/tools/posts/PostToolDefinitions.d.ts.map +1 -1
  168. package/dist/tools/posts/index.d.ts.map +1 -1
  169. package/dist/tools/seo/BulkOperations.d.ts +113 -0
  170. package/dist/tools/seo/BulkOperations.d.ts.map +1 -0
  171. package/dist/tools/seo/BulkOperations.js +398 -0
  172. package/dist/tools/seo/BulkOperations.js.map +1 -0
  173. package/dist/tools/seo/SEOHandlers.d.ts +55 -0
  174. package/dist/tools/seo/SEOHandlers.d.ts.map +1 -0
  175. package/dist/tools/seo/SEOHandlers.js +255 -0
  176. package/dist/tools/seo/SEOHandlers.js.map +1 -0
  177. package/dist/tools/seo/SEOToolDefinitions.d.ts +59 -0
  178. package/dist/tools/seo/SEOToolDefinitions.d.ts.map +1 -0
  179. package/dist/tools/seo/SEOToolDefinitions.js +385 -0
  180. package/dist/tools/seo/SEOToolDefinitions.js.map +1 -0
  181. package/dist/tools/seo/SEOTools.d.ts +203 -0
  182. package/dist/tools/seo/SEOTools.d.ts.map +1 -0
  183. package/dist/tools/seo/SEOTools.js +708 -0
  184. package/dist/tools/seo/SEOTools.js.map +1 -0
  185. package/dist/tools/seo/analyzers/ContentAnalyzer.d.ts +94 -0
  186. package/dist/tools/seo/analyzers/ContentAnalyzer.d.ts.map +1 -0
  187. package/dist/tools/seo/analyzers/ContentAnalyzer.js +402 -0
  188. package/dist/tools/seo/analyzers/ContentAnalyzer.js.map +1 -0
  189. package/dist/tools/seo/auditors/SiteAuditor.d.ts +121 -0
  190. package/dist/tools/seo/auditors/SiteAuditor.d.ts.map +1 -0
  191. package/dist/tools/seo/auditors/SiteAuditor.js +600 -0
  192. package/dist/tools/seo/auditors/SiteAuditor.js.map +1 -0
  193. package/dist/tools/seo/generators/MetaGenerator.d.ts +128 -0
  194. package/dist/tools/seo/generators/MetaGenerator.d.ts.map +1 -0
  195. package/dist/tools/seo/generators/MetaGenerator.js +547 -0
  196. package/dist/tools/seo/generators/MetaGenerator.js.map +1 -0
  197. package/dist/tools/seo/generators/SchemaGenerator.d.ts +204 -0
  198. package/dist/tools/seo/generators/SchemaGenerator.d.ts.map +1 -0
  199. package/dist/tools/seo/generators/SchemaGenerator.js +670 -0
  200. package/dist/tools/seo/generators/SchemaGenerator.js.map +1 -0
  201. package/dist/tools/seo/index.d.ts +17 -0
  202. package/dist/tools/seo/index.d.ts.map +1 -0
  203. package/dist/tools/seo/index.js +18 -0
  204. package/dist/tools/seo/index.js.map +1 -0
  205. package/dist/tools/seo/optimizers/InternalLinkingSuggester.d.ts +186 -0
  206. package/dist/tools/seo/optimizers/InternalLinkingSuggester.d.ts.map +1 -0
  207. package/dist/tools/seo/optimizers/InternalLinkingSuggester.js +683 -0
  208. package/dist/tools/seo/optimizers/InternalLinkingSuggester.js.map +1 -0
  209. package/dist/tools/site.d.ts.map +1 -1
  210. package/dist/tools/site.js +12 -12
  211. package/dist/tools/site.js.map +1 -1
  212. package/dist/tools/taxonomies.d.ts.map +1 -1
  213. package/dist/tools/taxonomies.js +20 -20
  214. package/dist/tools/taxonomies.js.map +1 -1
  215. package/dist/tools/users.d.ts.map +1 -1
  216. package/dist/tools/users.js +12 -12
  217. package/dist/tools/users.js.map +1 -1
  218. package/dist/types/client.d.ts +8 -6
  219. package/dist/types/client.d.ts.map +1 -1
  220. package/dist/types/client.js.map +1 -1
  221. package/dist/types/seo.d.ts +473 -0
  222. package/dist/types/seo.d.ts.map +1 -0
  223. package/dist/types/seo.js +94 -0
  224. package/dist/types/seo.js.map +1 -0
  225. package/dist/utils/enhancedError.js +1 -1
  226. package/dist/utils/enhancedError.js.map +1 -1
  227. package/dist/utils/error.d.ts.map +1 -1
  228. package/dist/utils/error.js +0 -1
  229. package/dist/utils/error.js.map +1 -1
  230. package/dist/utils/index.d.ts +12 -0
  231. package/dist/utils/index.d.ts.map +1 -0
  232. package/dist/utils/index.js +18 -0
  233. package/dist/utils/index.js.map +1 -0
  234. package/dist/utils/logger.js +3 -3
  235. package/dist/utils/logger.js.map +1 -1
  236. package/dist/utils/toolWrapper.d.ts +2 -2
  237. package/dist/utils/toolWrapper.js +8 -8
  238. package/dist/utils/toolWrapper.js.map +1 -1
  239. package/dist/utils/validation/core.d.ts.map +1 -1
  240. package/dist/utils/validation/core.js.map +1 -1
  241. package/dist/utils/validation/index.d.ts.map +1 -1
  242. package/dist/utils/validation/index.js.map +1 -1
  243. package/dist/utils/validation/network.js +3 -3
  244. package/dist/utils/validation/network.js.map +1 -1
  245. package/dist/utils/validation/rateLimit.js.map +1 -1
  246. package/dist/utils/validation/security.js.map +1 -1
  247. package/dist/utils/validation/wordpress.js.map +1 -1
  248. package/dist/utils/version.d.ts +144 -0
  249. package/dist/utils/version.d.ts.map +1 -0
  250. package/dist/utils/version.js +318 -0
  251. package/dist/utils/version.js.map +1 -0
  252. package/package.json +24 -58
  253. package/src/cache/CacheInvalidation.ts +183 -20
  254. package/src/cache/HttpCacheWrapper.ts +8 -5
  255. package/src/cache/SEOCacheManager.ts +330 -0
  256. package/src/cache/__tests__/CacheInvalidation.test.ts +6 -11
  257. package/src/cache/__tests__/CachedWordPressClient.test.ts +37 -62
  258. package/src/client/SEOWordPressClient.ts +876 -0
  259. package/src/client/api.ts +50 -21
  260. package/src/client/auth.ts +19 -19
  261. package/src/client/index.ts +16 -0
  262. package/src/client/managers/AuthManager.ts +175 -0
  263. package/src/client/managers/AuthenticationManager.ts +16 -14
  264. package/src/client/managers/BaseManager.ts +24 -5
  265. package/src/client/managers/ComposedAuthenticationManager.ts +409 -0
  266. package/src/client/managers/ComposedManagerFactory.ts +231 -0
  267. package/src/client/managers/ComposedRequestManager.ts +336 -0
  268. package/src/client/managers/JWTAuthImplementation.ts +326 -0
  269. package/src/client/managers/ManagersIndex.ts +6 -0
  270. package/src/client/managers/RequestManager.ts +9 -7
  271. package/src/client/managers/composed/MigrationAdapter.ts +263 -0
  272. package/src/client/managers/composed/index.ts +47 -0
  273. package/src/client/managers/implementations/ConfigurationProviderImpl.ts +52 -0
  274. package/src/client/managers/implementations/ErrorHandlerImpl.ts +102 -0
  275. package/src/client/managers/implementations/ParameterValidatorImpl.ts +221 -0
  276. package/src/client/managers/interfaces/ManagerInterfaces.ts +171 -0
  277. package/src/config/Config.ts +63 -0
  278. package/src/config/ConfigurationSchema.ts +17 -17
  279. package/src/config/ServerConfiguration.ts +18 -16
  280. package/src/config/index.ts +13 -0
  281. package/src/docs/DocumentationGenerator.ts +2 -2
  282. package/src/dxt-entry.ts +3 -3
  283. package/src/index.ts +43 -43
  284. package/src/performance/MetricsCollector.ts +15 -11
  285. package/src/security/AISecurityScanner.ts +7 -7
  286. package/src/security/AutomatedRemediation.ts +13 -11
  287. package/src/security/InputValidator.ts +10 -9
  288. package/src/security/SecurityCIPipeline.ts +494 -56
  289. package/src/security/SecurityConfigManager.ts +10 -10
  290. package/src/security/SecurityMonitoring.ts +5 -5
  291. package/src/security/SecurityReviewer.ts +13 -6
  292. package/src/security/index.ts +3 -3
  293. package/src/server/ConnectionTester.ts +5 -5
  294. package/src/server/ToolRegistry.ts +9 -8
  295. package/src/server/index.ts +10 -0
  296. package/src/tools/BaseToolManager.ts +55 -83
  297. package/src/tools/auth.ts +21 -12
  298. package/src/tools/comments.ts +23 -19
  299. package/src/tools/index.ts +1 -0
  300. package/src/tools/media.ts +23 -20
  301. package/src/tools/pages.ts +20 -13
  302. package/src/tools/performance.ts +101 -32
  303. package/src/tools/posts/PostHandlers.ts +23 -23
  304. package/src/tools/posts/PostToolDefinitions.ts +1 -1
  305. package/src/tools/posts/index.ts +2 -2
  306. package/src/tools/seo/BulkOperations.ts +557 -0
  307. package/src/tools/seo/SEOHandlers.ts +296 -0
  308. package/src/tools/seo/SEOToolDefinitions.ts +402 -0
  309. package/src/tools/seo/SEOTools.ts +871 -0
  310. package/src/tools/seo/analyzers/ContentAnalyzer.ts +493 -0
  311. package/src/tools/seo/auditors/SiteAuditor.ts +787 -0
  312. package/src/tools/seo/generators/MetaGenerator.ts +694 -0
  313. package/src/tools/seo/generators/SchemaGenerator.ts +955 -0
  314. package/src/tools/seo/index.ts +47 -0
  315. package/src/tools/seo/optimizers/InternalLinkingSuggester.ts +934 -0
  316. package/src/tools/site.ts +27 -26
  317. package/src/tools/taxonomies.ts +29 -25
  318. package/src/tools/users.ts +20 -13
  319. package/src/types/client.ts +8 -6
  320. package/src/types/seo.ts +546 -0
  321. package/src/utils/enhancedError.ts +1 -1
  322. package/src/utils/error.ts +1 -2
  323. package/src/utils/index.ts +23 -0
  324. package/src/utils/logger.ts +3 -3
  325. package/src/utils/toolWrapper.ts +10 -10
  326. package/src/utils/validation/core.ts +2 -2
  327. package/src/utils/validation/index.ts +2 -2
  328. package/src/utils/validation/network.ts +5 -5
  329. package/src/utils/validation/rateLimit.ts +1 -1
  330. package/src/utils/validation/security.ts +1 -1
  331. package/src/utils/validation/wordpress.ts +1 -1
  332. package/src/utils/version.ts +402 -0
  333. package/src/dxt-entry.cjs +0 -68
@@ -0,0 +1,934 @@
1
+ /**
2
+ * Internal Linking Suggester
3
+ *
4
+ * This module analyzes WordPress content to suggest relevant internal linking
5
+ * opportunities based on semantic analysis, keyword matching, and content clustering.
6
+ * It helps improve site architecture, user navigation, and SEO link equity distribution.
7
+ *
8
+ * Features:
9
+ * - Semantic content analysis for relevance scoring
10
+ * - Keyword-based link suggestions with confidence metrics
11
+ * - Topic clustering for hub-and-spoke content architecture
12
+ * - Anchor text optimization recommendations
13
+ * - Link density and distribution analysis
14
+ * - Context-aware link placement suggestions
15
+ *
16
+ * @since 2.7.0
17
+ */
18
+
19
+ import { WordPressClient } from "../../../client/api.js";
20
+ import { LoggerFactory } from "../../../utils/logger.js";
21
+ import type { InternalLinkSuggestion, SEOToolParams } from "../../../types/seo.js";
22
+ import type { WordPressPost } from "../../../types/wordpress.js";
23
+
24
+ /**
25
+ * Configuration for internal linking suggestions
26
+ */
27
+ interface LinkingSuggestionConfig {
28
+ /** Maximum number of suggestions to return */
29
+ maxSuggestions: number;
30
+
31
+ /** Minimum relevance score threshold (0-100) */
32
+ minRelevanceScore: number;
33
+
34
+ /** Maximum links per post to suggest */
35
+ maxLinksPerPost: number;
36
+
37
+ /** Enable semantic analysis */
38
+ useSemanticAnalysis: boolean;
39
+
40
+ /** Include category-based suggestions */
41
+ includeCategoryMatches: boolean;
42
+
43
+ /** Include tag-based suggestions */
44
+ includeTagMatches: boolean;
45
+
46
+ /** Minimum word count for target posts */
47
+ minWordCount: number;
48
+
49
+ /** Exclude posts older than X days */
50
+ maxPostAge: number;
51
+
52
+ /** Enable contextual placement suggestions */
53
+ enableContextualPlacement: boolean;
54
+ }
55
+
56
+ /**
57
+ * Content analysis result for relevance scoring
58
+ */
59
+ interface ContentAnalysis {
60
+ /** Extracted keywords with frequency */
61
+ keywords: Array<{ word: string; frequency: number; tfidf: number }>;
62
+
63
+ /** Main topics identified in content */
64
+ topics: string[];
65
+
66
+ /** Content category based on analysis */
67
+ category: string;
68
+
69
+ /** Semantic fingerprint for similarity matching */
70
+ semanticFingerprint: number[];
71
+
72
+ /** Word count */
73
+ wordCount: number;
74
+
75
+ /** Reading level */
76
+ readingLevel: number;
77
+ }
78
+
79
+ /**
80
+ * Link placement context
81
+ */
82
+ interface LinkPlacementContext {
83
+ /** Paragraph index where link could be placed */
84
+ paragraphIndex: number;
85
+
86
+ /** Character position within paragraph */
87
+ characterPosition: number;
88
+
89
+ /** Surrounding text context */
90
+ contextBefore: string;
91
+
92
+ /** Surrounding text context */
93
+ contextAfter: string;
94
+
95
+ /** Suggested anchor text */
96
+ suggestedAnchor: string;
97
+
98
+ /** Confidence score for this placement (0-100) */
99
+ placementScore: number;
100
+ }
101
+
102
+ /**
103
+ * Topic cluster information
104
+ */
105
+ interface TopicCluster {
106
+ /** Cluster identifier */
107
+ clusterId: string;
108
+
109
+ /** Main topic/theme */
110
+ topic: string;
111
+
112
+ /** Posts in this cluster */
113
+ posts: Array<{
114
+ postId: number;
115
+ title: string;
116
+ url: string;
117
+ relevanceScore: number;
118
+ isHub: boolean;
119
+ }>;
120
+
121
+ /** Cluster coherence score */
122
+ coherenceScore: number;
123
+
124
+ /** Suggested hub post (if any) */
125
+ hubPost?: number;
126
+ }
127
+
128
+ /**
129
+ * Internal Linking Suggester Class
130
+ */
131
+ export class InternalLinkingSuggester {
132
+ private logger = LoggerFactory.tool("internal_linking");
133
+ private config: LinkingSuggestionConfig;
134
+
135
+ constructor(
136
+ private client: WordPressClient,
137
+ config?: Partial<LinkingSuggestionConfig>,
138
+ ) {
139
+ // Default configuration
140
+ this.config = {
141
+ maxSuggestions: 10,
142
+ minRelevanceScore: 30,
143
+ maxLinksPerPost: 5,
144
+ useSemanticAnalysis: true,
145
+ includeCategoryMatches: true,
146
+ includeTagMatches: true,
147
+ minWordCount: 300,
148
+ maxPostAge: 365, // 1 year
149
+ enableContextualPlacement: true,
150
+ ...config,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Generate internal linking suggestions for a specific post
156
+ */
157
+ async generateSuggestions(sourcePost: WordPressPost, params: SEOToolParams): Promise<InternalLinkSuggestion[]> {
158
+ this.logger.debug("Generating internal linking suggestions", {
159
+ postId: sourcePost.id,
160
+ title: sourcePost.title?.rendered?.substring(0, 50),
161
+ maxSuggestions: this.config.maxSuggestions,
162
+ });
163
+
164
+ try {
165
+ // Analyze source post content
166
+ const sourceAnalysis = await this.analyzePostContent(sourcePost);
167
+
168
+ // Get candidate posts for linking
169
+ const candidatePosts = await this.getCandidatePosts(sourcePost, params);
170
+
171
+ // Analyze candidate posts
172
+ const candidateAnalyses = await Promise.all(candidatePosts.map((post) => this.analyzePostContent(post)));
173
+
174
+ // Calculate relevance scores
175
+ const scoredSuggestions = this.calculateRelevanceScores(
176
+ sourcePost,
177
+ sourceAnalysis,
178
+ candidatePosts,
179
+ candidateAnalyses,
180
+ );
181
+
182
+ // Filter and rank suggestions
183
+ const filteredSuggestions = scoredSuggestions
184
+ .filter((suggestion) => suggestion.relevance >= this.config.minRelevanceScore)
185
+ .sort((a, b) => b.relevance - a.relevance)
186
+ .slice(0, this.config.maxSuggestions);
187
+
188
+ // Add contextual placement information
189
+ const enhancedSuggestions = await Promise.all(
190
+ filteredSuggestions.map((suggestion) => this.enhanceWithContextualPlacement(sourcePost, suggestion)),
191
+ );
192
+
193
+ this.logger.info("Generated internal linking suggestions", {
194
+ sourcePostId: sourcePost.id,
195
+ candidatesAnalyzed: candidatePosts.length,
196
+ suggestionsFound: enhancedSuggestions.length,
197
+ avgRelevanceScore:
198
+ enhancedSuggestions.length > 0
199
+ ? (enhancedSuggestions.reduce((sum, s) => sum + s.relevance, 0) / enhancedSuggestions.length).toFixed(1)
200
+ : 0,
201
+ });
202
+
203
+ return enhancedSuggestions;
204
+ } catch (_error) {
205
+ this.logger.error("Failed to generate internal linking suggestions", {
206
+ postId: sourcePost.id,
207
+ _error: _error instanceof Error ? _error.message : String(_error),
208
+ });
209
+ throw _error;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Analyze content clustering for site-wide link architecture
215
+ */
216
+ async analyzeContentClusters(params: SEOToolParams): Promise<TopicCluster[]> {
217
+ this.logger.debug("Analyzing content clusters", {
218
+ site: params.site,
219
+ });
220
+
221
+ try {
222
+ // Get all published posts
223
+ const allPosts = await this.getAllPosts(params);
224
+
225
+ // Analyze all posts
226
+ const postAnalyses = await Promise.all(allPosts.map((post) => this.analyzePostContent(post)));
227
+
228
+ // Create topic clusters using similarity analysis
229
+ const clusters = this.createTopicClusters(allPosts, postAnalyses);
230
+
231
+ // Identify hub posts for each cluster
232
+ const enhancedClusters = clusters.map((cluster) => this.identifyHubPost(cluster));
233
+
234
+ this.logger.info("Content clustering analysis completed", {
235
+ totalPosts: allPosts.length,
236
+ clustersFound: enhancedClusters.length,
237
+ avgClusterSize: enhancedClusters.length > 0 ? (allPosts.length / enhancedClusters.length).toFixed(1) : 0,
238
+ });
239
+
240
+ return enhancedClusters;
241
+ } catch (_error) {
242
+ this.logger.error("Failed to analyze content clusters", {
243
+ _error: _error instanceof Error ? _error.message : String(_error),
244
+ });
245
+ throw _error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get bulk linking suggestions for multiple posts
251
+ */
252
+ async generateBulkSuggestions(
253
+ postIds: number[],
254
+ params: SEOToolParams,
255
+ ): Promise<Array<{ postId: number; suggestions: InternalLinkSuggestion[] }>> {
256
+ this.logger.debug("Generating bulk internal linking suggestions", {
257
+ postCount: postIds.length,
258
+ });
259
+
260
+ const results = [];
261
+
262
+ for (const postId of postIds) {
263
+ try {
264
+ const post = await this.client.getPost(postId);
265
+ if (post) {
266
+ const suggestions = await this.generateSuggestions(post as WordPressPost, params);
267
+ results.push({ postId, suggestions });
268
+ }
269
+ } catch (_error) {
270
+ this.logger.warn("Failed to generate suggestions for post", {
271
+ postId,
272
+ _error: _error instanceof Error ? _error.message : String(_error),
273
+ });
274
+ results.push({ postId, suggestions: [] });
275
+ }
276
+ }
277
+
278
+ return results;
279
+ }
280
+
281
+ /**
282
+ * Analyze post content for relevance scoring
283
+ */
284
+ private async analyzePostContent(post: WordPressPost): Promise<ContentAnalysis> {
285
+ const content = this.extractTextContent(post.content?.rendered || "");
286
+ const title = post.title?.rendered || "";
287
+ const fullText = `${title} ${content}`;
288
+
289
+ // Extract keywords with TF-IDF scoring
290
+ const keywords = this.extractKeywords(fullText);
291
+
292
+ // Identify main topics
293
+ const topics = this.extractTopics(fullText, keywords);
294
+
295
+ // Determine content category
296
+ const category = this.categorizeContent(fullText, keywords);
297
+
298
+ // Generate semantic fingerprint
299
+ const semanticFingerprint = this.generateSemanticFingerprint(fullText, keywords);
300
+
301
+ return {
302
+ keywords,
303
+ topics,
304
+ category,
305
+ semanticFingerprint,
306
+ wordCount: this.countWords(content),
307
+ readingLevel: this.calculateReadingLevel(content),
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Get candidate posts for internal linking
313
+ */
314
+ private async getCandidatePosts(sourcePost: WordPressPost, params: SEOToolParams): Promise<WordPressPost[]> {
315
+ try {
316
+ // Get all published posts except the source post
317
+ const allPosts = await this.getAllPosts(params);
318
+
319
+ return allPosts.filter((post) => {
320
+ // Exclude source post
321
+ if (post.id === sourcePost.id) return false;
322
+
323
+ // Only include published posts
324
+ if (post.status !== "publish") return false;
325
+
326
+ // Check minimum word count
327
+ const wordCount = this.countWords(this.extractTextContent(post.content?.rendered || ""));
328
+ if (wordCount < this.config.minWordCount) return false;
329
+
330
+ // Check post age if specified
331
+ if (this.config.maxPostAge > 0) {
332
+ const postDate = new Date(post.date || "");
333
+ const daysSincePost = (Date.now() - postDate.getTime()) / (1000 * 60 * 60 * 24);
334
+ if (daysSincePost > this.config.maxPostAge) return false;
335
+ }
336
+
337
+ return true;
338
+ });
339
+ } catch (_error) {
340
+ this.logger.error("Failed to get candidate posts", {
341
+ _error: _error instanceof Error ? _error.message : String(_error),
342
+ });
343
+ throw _error; // Re-throw error to maintain error propagation
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Get all published posts
349
+ */
350
+ async getAllPosts(params: SEOToolParams): Promise<WordPressPost[]> {
351
+ // In a real implementation, this would fetch from WordPress API
352
+ // const response = await this.client.getPosts({ per_page: 100, status: 'publish' });
353
+ // return response as WordPressPost[];
354
+
355
+ // For now, return empty array (tests will mock this method)
356
+ return [];
357
+ }
358
+
359
+ /**
360
+ * Calculate relevance scores between source and candidate posts
361
+ */
362
+ private calculateRelevanceScores(
363
+ sourcePost: WordPressPost,
364
+ sourceAnalysis: ContentAnalysis,
365
+ candidatePosts: WordPressPost[],
366
+ candidateAnalyses: ContentAnalysis[],
367
+ ): InternalLinkSuggestion[] {
368
+ return candidatePosts.map((candidatePost, index) => {
369
+ const candidateAnalysis = candidateAnalyses[index];
370
+
371
+ // Calculate various relevance factors
372
+ const keywordSimilarity = this.calculateKeywordSimilarity(sourceAnalysis.keywords, candidateAnalysis.keywords);
373
+
374
+ const topicSimilarity = this.calculateTopicSimilarity(sourceAnalysis.topics, candidateAnalysis.topics);
375
+
376
+ const semanticSimilarity = this.calculateSemanticSimilarity(
377
+ sourceAnalysis.semanticFingerprint,
378
+ candidateAnalysis.semanticFingerprint,
379
+ );
380
+
381
+ const categorySimilarity = sourceAnalysis.category === candidateAnalysis.category ? 0.3 : 0;
382
+
383
+ // Weighted relevance score
384
+ const relevanceScore = Math.round(
385
+ keywordSimilarity * 0.4 + topicSimilarity * 0.3 + semanticSimilarity * 0.2 + categorySimilarity * 0.1,
386
+ );
387
+
388
+ // Generate suggested anchor text
389
+ const anchorText = this.generateAnchorText(candidatePost, sourceAnalysis.keywords);
390
+
391
+ // Determine reason for suggestion
392
+ const reason = this.generateSuggestionReason(
393
+ keywordSimilarity,
394
+ topicSimilarity,
395
+ semanticSimilarity,
396
+ categorySimilarity,
397
+ );
398
+
399
+ return {
400
+ sourcePostId: sourcePost.id,
401
+ targetPostId: candidatePost.id,
402
+ targetTitle: candidatePost.title?.rendered || "Untitled",
403
+ targetUrl: candidatePost.link || `#${candidatePost.id}`,
404
+ anchorText,
405
+ relevance: relevanceScore,
406
+ reason,
407
+ context: "", // Will be filled by contextual placement
408
+ };
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Enhance suggestions with contextual placement information
414
+ */
415
+ private async enhanceWithContextualPlacement(
416
+ sourcePost: WordPressPost,
417
+ suggestion: InternalLinkSuggestion,
418
+ ): Promise<InternalLinkSuggestion> {
419
+ if (!this.config.enableContextualPlacement) {
420
+ return suggestion;
421
+ }
422
+
423
+ const content = sourcePost.content?.rendered || "";
424
+ const placements = this.findOptimalPlacements(content, suggestion.anchorText, suggestion.targetTitle);
425
+
426
+ if (placements.length > 0) {
427
+ const bestPlacement = placements[0];
428
+ suggestion.context = `${bestPlacement.contextBefore}[${suggestion.anchorText}]${bestPlacement.contextAfter}`;
429
+ }
430
+
431
+ return suggestion;
432
+ }
433
+
434
+ /**
435
+ * Create topic clusters from analyzed posts
436
+ */
437
+ private createTopicClusters(posts: WordPressPost[], analyses: ContentAnalysis[]): TopicCluster[] {
438
+ const clusters: TopicCluster[] = [];
439
+ const clustered = new Set<number>();
440
+
441
+ analyses.forEach((analysis, index) => {
442
+ if (clustered.has(index)) return;
443
+
444
+ const post = posts[index];
445
+ const cluster: TopicCluster = {
446
+ clusterId: `cluster_${clusters.length + 1}`,
447
+ topic: analysis.topics[0] || analysis.category,
448
+ posts: [
449
+ {
450
+ postId: post.id,
451
+ title: post.title?.rendered || "Untitled",
452
+ url: post.link || `#${post.id}`,
453
+ relevanceScore: 100,
454
+ isHub: false,
455
+ },
456
+ ],
457
+ coherenceScore: 0,
458
+ };
459
+
460
+ // Find similar posts
461
+ analyses.forEach((otherAnalysis, otherIndex) => {
462
+ if (otherIndex === index || clustered.has(otherIndex)) return;
463
+
464
+ const similarity = this.calculateSemanticSimilarity(
465
+ analysis.semanticFingerprint,
466
+ otherAnalysis.semanticFingerprint,
467
+ );
468
+
469
+ if (similarity > 60) {
470
+ // Similarity threshold for clustering
471
+ const otherPost = posts[otherIndex];
472
+ cluster.posts.push({
473
+ postId: otherPost.id,
474
+ title: otherPost.title?.rendered || "Untitled",
475
+ url: otherPost.link || `#${otherPost.id}`,
476
+ relevanceScore: similarity,
477
+ isHub: false,
478
+ });
479
+ clustered.add(otherIndex);
480
+ }
481
+ });
482
+
483
+ if (cluster.posts.length > 1) {
484
+ clusters.push(cluster);
485
+ clustered.add(index);
486
+ }
487
+ });
488
+
489
+ return clusters;
490
+ }
491
+
492
+ /**
493
+ * Identify hub post for a cluster
494
+ */
495
+ private identifyHubPost(cluster: TopicCluster): TopicCluster {
496
+ // Find the post with highest average relevance to other posts in cluster
497
+ let bestHubScore = 0;
498
+ let hubPostId = cluster.posts[0].postId;
499
+
500
+ cluster.posts.forEach((post) => {
501
+ const avgRelevance =
502
+ cluster.posts.filter((p) => p.postId !== post.postId).reduce((sum, p) => sum + p.relevanceScore, 0) /
503
+ (cluster.posts.length - 1);
504
+
505
+ if (avgRelevance > bestHubScore) {
506
+ bestHubScore = avgRelevance;
507
+ hubPostId = post.postId;
508
+ }
509
+ });
510
+
511
+ // Mark hub post
512
+ cluster.posts.forEach((post) => {
513
+ post.isHub = post.postId === hubPostId;
514
+ });
515
+
516
+ cluster.hubPost = hubPostId;
517
+ cluster.coherenceScore = bestHubScore;
518
+
519
+ return cluster;
520
+ }
521
+
522
+ // Utility methods for content analysis
523
+
524
+ /**
525
+ * Extract plain text from HTML content
526
+ */
527
+ private extractTextContent(html: string): string {
528
+ return html
529
+ .replace(/<[^>]*>/g, " ")
530
+ .replace(/\s+/g, " ")
531
+ .trim();
532
+ }
533
+
534
+ /**
535
+ * Count words in text
536
+ */
537
+ private countWords(text: string): number {
538
+ return text.split(/\s+/).filter((word) => word.length > 0).length;
539
+ }
540
+
541
+ /**
542
+ * Extract keywords with TF-IDF scoring
543
+ */
544
+ private extractKeywords(text: string): Array<{ word: string; frequency: number; tfidf: number }> {
545
+ const words = text
546
+ .toLowerCase()
547
+ .replace(/[^\w\s]/g, " ")
548
+ .split(/\s+/)
549
+ .filter((word) => word.length > 3)
550
+ .filter((word) => !this.isStopWord(word));
551
+
552
+ const wordCounts = new Map<string, number>();
553
+ words.forEach((word) => {
554
+ wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
555
+ });
556
+
557
+ const totalWords = words.length;
558
+ const uniqueWords = wordCounts.size;
559
+
560
+ return Array.from(wordCounts.entries())
561
+ .map(([word, count]) => ({
562
+ word,
563
+ frequency: count,
564
+ tfidf: (count / totalWords) * Math.log(uniqueWords / count), // Simplified TF-IDF
565
+ }))
566
+ .sort((a, b) => b.tfidf - a.tfidf)
567
+ .slice(0, 20); // Top 20 keywords
568
+ }
569
+
570
+ /**
571
+ * Check if word is a stop word
572
+ */
573
+ private isStopWord(word: string): boolean {
574
+ const stopWords = new Set([
575
+ "the",
576
+ "a",
577
+ "an",
578
+ "and",
579
+ "or",
580
+ "but",
581
+ "in",
582
+ "on",
583
+ "at",
584
+ "to",
585
+ "for",
586
+ "of",
587
+ "with",
588
+ "by",
589
+ "from",
590
+ "up",
591
+ "about",
592
+ "into",
593
+ "through",
594
+ "during",
595
+ "before",
596
+ "after",
597
+ "above",
598
+ "below",
599
+ "between",
600
+ "among",
601
+ "this",
602
+ "that",
603
+ "these",
604
+ "those",
605
+ "i",
606
+ "me",
607
+ "my",
608
+ "myself",
609
+ "we",
610
+ "our",
611
+ "ours",
612
+ "ourselves",
613
+ "you",
614
+ "your",
615
+ "yours",
616
+ "yourself",
617
+ "he",
618
+ "him",
619
+ "his",
620
+ "himself",
621
+ "she",
622
+ "her",
623
+ "hers",
624
+ "herself",
625
+ "it",
626
+ "its",
627
+ "itself",
628
+ "they",
629
+ "them",
630
+ "their",
631
+ "theirs",
632
+ "themselves",
633
+ "what",
634
+ "which",
635
+ "who",
636
+ "whom",
637
+ "whose",
638
+ "this",
639
+ "that",
640
+ "these",
641
+ "those",
642
+ "am",
643
+ "is",
644
+ "are",
645
+ "was",
646
+ "were",
647
+ "be",
648
+ "been",
649
+ "being",
650
+ "have",
651
+ "has",
652
+ "had",
653
+ "having",
654
+ "do",
655
+ "does",
656
+ "did",
657
+ "doing",
658
+ "will",
659
+ "would",
660
+ "should",
661
+ "could",
662
+ "can",
663
+ "may",
664
+ "might",
665
+ "must",
666
+ "shall",
667
+ ]);
668
+
669
+ return stopWords.has(word.toLowerCase());
670
+ }
671
+
672
+ /**
673
+ * Extract main topics from content
674
+ */
675
+ private extractTopics(text: string, keywords: Array<{ word: string; frequency: number; tfidf: number }>): string[] {
676
+ // Simple topic extraction based on high-frequency keywords
677
+ return keywords
678
+ .slice(0, 5)
679
+ .map((kw) => kw.word)
680
+ .filter((word) => word.length > 4); // Filter for meaningful topic words
681
+ }
682
+
683
+ /**
684
+ * Categorize content based on keywords and patterns
685
+ */
686
+ private categorizeContent(text: string, keywords: Array<{ word: string; frequency: number; tfidf: number }>): string {
687
+ const keywordSet = new Set(keywords.map((kw) => kw.word));
688
+
689
+ // Define category keywords
690
+ const categories = {
691
+ tutorial: ["tutorial", "guide", "how", "step", "learn", "beginners", "basics"],
692
+ review: ["review", "comparison", "versus", "pros", "cons", "rating", "opinion"],
693
+ news: ["news", "update", "announcement", "breaking", "latest", "recent"],
694
+ technical: ["code", "programming", "development", "api", "technical", "implementation"],
695
+ business: ["business", "marketing", "strategy", "growth", "revenue", "profit"],
696
+ general: [],
697
+ };
698
+
699
+ let bestMatch = "general";
700
+ let bestScore = 0;
701
+
702
+ Object.entries(categories).forEach(([category, categoryKeywords]) => {
703
+ const matches = categoryKeywords.filter((kw) => keywordSet.has(kw)).length;
704
+ if (matches > bestScore) {
705
+ bestScore = matches;
706
+ bestMatch = category;
707
+ }
708
+ });
709
+
710
+ return bestMatch;
711
+ }
712
+
713
+ /**
714
+ * Generate semantic fingerprint for similarity comparison
715
+ */
716
+ private generateSemanticFingerprint(
717
+ text: string,
718
+ keywords: Array<{ word: string; frequency: number; tfidf: number }>,
719
+ ): number[] {
720
+ // Simple semantic fingerprint based on keyword TF-IDF scores
721
+ const fingerprint = new Array(50).fill(0);
722
+
723
+ keywords.slice(0, 50).forEach((keyword, index) => {
724
+ fingerprint[index] = keyword.tfidf;
725
+ });
726
+
727
+ return fingerprint;
728
+ }
729
+
730
+ /**
731
+ * Calculate reading level (simplified Flesch-Kincaid)
732
+ */
733
+ private calculateReadingLevel(text: string): number {
734
+ const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
735
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
736
+ const syllables = words.reduce((count, word) => count + this.countSyllables(word), 0);
737
+
738
+ if (sentences.length === 0 || words.length === 0) return 0;
739
+
740
+ const avgWordsPerSentence = words.length / sentences.length;
741
+ const avgSyllablesPerWord = syllables / words.length;
742
+
743
+ return 206.835 - 1.015 * avgWordsPerSentence - 84.6 * avgSyllablesPerWord;
744
+ }
745
+
746
+ /**
747
+ * Count syllables in a word (simplified)
748
+ */
749
+ private countSyllables(word: string): number {
750
+ word = word.toLowerCase();
751
+ if (word.length <= 3) return 1;
752
+
753
+ const vowels = word.match(/[aeiouy]+/g);
754
+ let syllableCount = vowels ? vowels.length : 1;
755
+
756
+ // Adjust for silent e
757
+ if (word.endsWith("e")) {
758
+ syllableCount--;
759
+ }
760
+
761
+ return Math.max(syllableCount, 1);
762
+ }
763
+
764
+ /**
765
+ * Calculate keyword similarity between two posts
766
+ */
767
+ private calculateKeywordSimilarity(
768
+ keywords1: Array<{ word: string; frequency: number; tfidf: number }>,
769
+ keywords2: Array<{ word: string; frequency: number; tfidf: number }>,
770
+ ): number {
771
+ const set1 = new Set(keywords1.map((kw) => kw.word));
772
+ const set2 = new Set(keywords2.map((kw) => kw.word));
773
+
774
+ const intersection = new Set([...set1].filter((word) => set2.has(word)));
775
+ const union = new Set([...set1, ...set2]);
776
+
777
+ return union.size > 0 ? (intersection.size / union.size) * 100 : 0;
778
+ }
779
+
780
+ /**
781
+ * Calculate topic similarity between two posts
782
+ */
783
+ private calculateTopicSimilarity(topics1: string[], topics2: string[]): number {
784
+ const set1 = new Set(topics1);
785
+ const set2 = new Set(topics2);
786
+
787
+ const intersection = new Set([...set1].filter((topic) => set2.has(topic)));
788
+ const union = new Set([...set1, ...set2]);
789
+
790
+ return union.size > 0 ? (intersection.size / union.size) * 100 : 0;
791
+ }
792
+
793
+ /**
794
+ * Calculate semantic similarity using fingerprints
795
+ */
796
+ private calculateSemanticSimilarity(fingerprint1: number[], fingerprint2: number[]): number {
797
+ // Cosine similarity
798
+ let dotProduct = 0;
799
+ let norm1 = 0;
800
+ let norm2 = 0;
801
+
802
+ for (let i = 0; i < Math.min(fingerprint1.length, fingerprint2.length); i++) {
803
+ dotProduct += fingerprint1[i] * fingerprint2[i];
804
+ norm1 += fingerprint1[i] * fingerprint1[i];
805
+ norm2 += fingerprint2[i] * fingerprint2[i];
806
+ }
807
+
808
+ const magnitude = Math.sqrt(norm1) * Math.sqrt(norm2);
809
+ return magnitude > 0 ? (dotProduct / magnitude) * 100 : 0;
810
+ }
811
+
812
+ /**
813
+ * Generate suggested anchor text for a link
814
+ */
815
+ private generateAnchorText(
816
+ targetPost: WordPressPost,
817
+ sourceKeywords: Array<{ word: string; frequency: number; tfidf: number }>,
818
+ ): string {
819
+ const title = targetPost.title?.rendered || "Untitled";
820
+
821
+ // Try to find relevant keywords from source in target title
822
+ const relevantKeywords = sourceKeywords
823
+ .filter((kw) => title.toLowerCase().includes(kw.word.toLowerCase()))
824
+ .slice(0, 3);
825
+
826
+ if (relevantKeywords.length > 0) {
827
+ // Use the most relevant keyword phrase from title
828
+ const keyword = relevantKeywords[0].word;
829
+ const titleWords = title.toLowerCase().split(/\s+/);
830
+ const keywordIndex = titleWords.findIndex((word) => word.includes(keyword.toLowerCase()));
831
+
832
+ if (keywordIndex >= 0) {
833
+ // Return 2-3 words around the keyword
834
+ const start = Math.max(0, keywordIndex - 1);
835
+ const end = Math.min(titleWords.length, keywordIndex + 2);
836
+ return titleWords.slice(start, end).join(" ");
837
+ }
838
+ }
839
+
840
+ // Fallback to title (truncated if necessary)
841
+ return title.length > 50 ? title.substring(0, 47) + "..." : title;
842
+ }
843
+
844
+ /**
845
+ * Generate human-readable reason for suggestion
846
+ */
847
+ private generateSuggestionReason(
848
+ keywordSim: number,
849
+ topicSim: number,
850
+ semanticSim: number,
851
+ categorySim: number,
852
+ ): string {
853
+ if (keywordSim > 50) {
854
+ return "Strong keyword overlap suggests high relevance";
855
+ } else if (topicSim > 60) {
856
+ return "Similar topics make this a good contextual link";
857
+ } else if (semanticSim > 40) {
858
+ return "Semantic content analysis indicates relevance";
859
+ } else if (categorySim > 0) {
860
+ return "Same content category suggests related information";
861
+ } else {
862
+ return "General content relevance detected";
863
+ }
864
+ }
865
+
866
+ /**
867
+ * Find optimal placements for internal links within content
868
+ */
869
+ private findOptimalPlacements(content: string, anchorText: string, targetTitle: string): LinkPlacementContext[] {
870
+ const placements: LinkPlacementContext[] = [];
871
+
872
+ // Extract paragraphs
873
+ const paragraphs = content
874
+ .split(/<\/p>/i)
875
+ .map((p) => this.extractTextContent(p).trim())
876
+ .filter((p) => p.length > 50);
877
+
878
+ paragraphs.forEach((paragraph, pIndex) => {
879
+ // Look for relevant keywords or phrases
880
+ const lowerParagraph = paragraph.toLowerCase();
881
+ const lowerAnchor = anchorText.toLowerCase();
882
+ const lowerTitle = targetTitle.toLowerCase();
883
+
884
+ // Check for exact anchor text match
885
+ let position = lowerParagraph.indexOf(lowerAnchor);
886
+ if (position >= 0) {
887
+ placements.push({
888
+ paragraphIndex: pIndex,
889
+ characterPosition: position,
890
+ contextBefore: paragraph.substring(Math.max(0, position - 30), position),
891
+ contextAfter: paragraph.substring(position + anchorText.length, position + anchorText.length + 30),
892
+ suggestedAnchor: anchorText,
893
+ placementScore: 90,
894
+ });
895
+ return;
896
+ }
897
+
898
+ // Look for title keywords
899
+ const titleWords = lowerTitle.split(/\s+/);
900
+ titleWords.forEach((word) => {
901
+ if (word.length > 4) {
902
+ position = lowerParagraph.indexOf(word);
903
+ if (position >= 0 && placements.length < 3) {
904
+ placements.push({
905
+ paragraphIndex: pIndex,
906
+ characterPosition: position,
907
+ contextBefore: paragraph.substring(Math.max(0, position - 30), position),
908
+ contextAfter: paragraph.substring(position + word.length, position + word.length + 30),
909
+ suggestedAnchor: word,
910
+ placementScore: 70,
911
+ });
912
+ }
913
+ }
914
+ });
915
+ });
916
+
917
+ return placements.sort((a, b) => b.placementScore - a.placementScore).slice(0, 3);
918
+ }
919
+
920
+ /**
921
+ * Get current configuration
922
+ */
923
+ getConfig(): LinkingSuggestionConfig {
924
+ return { ...this.config };
925
+ }
926
+
927
+ /**
928
+ * Update configuration
929
+ */
930
+ updateConfig(config: Partial<LinkingSuggestionConfig>): void {
931
+ this.config = { ...this.config, ...config };
932
+ this.logger.debug("Configuration updated", { config: this.config });
933
+ }
934
+ }