mcp-wordpress 2.6.4 → 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 (332) 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 +21 -55
  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
@@ -0,0 +1,876 @@
1
+ /**
2
+ * SEO WordPress Client
3
+ *
4
+ * Extended WordPress REST API client with enhanced SEO capabilities.
5
+ * Provides specialized methods for SEO metadata management, schema markup,
6
+ * and integration with popular WordPress SEO plugins like Yoast and RankMath.
7
+ *
8
+ * Features:
9
+ * - SEO metadata retrieval and updates via REST API
10
+ * - Integration with Yoast SEO and RankMath plugin APIs
11
+ * - Schema markup management through WordPress custom fields
12
+ * - Bulk SEO operations with progress tracking
13
+ * - SEO-specific data normalization and validation
14
+ * - Plugin-agnostic metadata handling
15
+ *
16
+ * @since 2.7.0
17
+ */
18
+
19
+ import { WordPressClient } from "./api.js";
20
+ import { LoggerFactory } from "../utils/logger.js";
21
+ import { handleToolError } from "../utils/error.js";
22
+ import type { WordPressPost, WordPressPage } from "../types/wordpress.js";
23
+ import type { WordPressClientConfig } from "../types/client.js";
24
+ import type { SchemaType } from "../types/seo.js";
25
+ import type { SEOMetadata, SchemaMarkup } from "../types/seo.js";
26
+
27
+ /**
28
+ * WordPress Plugin interface for API responses
29
+ */
30
+ interface WordPressPlugin {
31
+ slug: string;
32
+ status: "active" | "inactive";
33
+ name?: string;
34
+ description?: string;
35
+ version?: string;
36
+ }
37
+
38
+ /**
39
+ * SEO plugin metadata field mappings
40
+ */
41
+ interface SEOPluginFields {
42
+ yoast: {
43
+ title: string;
44
+ description: string;
45
+ focusKeyword: string;
46
+ canonical: string;
47
+ noindex: string;
48
+ nofollow: string;
49
+ schema: string;
50
+ };
51
+ rankmath: {
52
+ title: string;
53
+ description: string;
54
+ focusKeyword: string;
55
+ canonical: string;
56
+ robots: string;
57
+ schema: string;
58
+ };
59
+ seopress: {
60
+ title: string;
61
+ description: string;
62
+ targetKeyword: string;
63
+ canonical: string;
64
+ robots: string;
65
+ };
66
+ none: {
67
+ title: string;
68
+ description: string;
69
+ focusKeyword: string;
70
+ canonical: string;
71
+ noindex: string;
72
+ nofollow: string;
73
+ schema: string;
74
+ };
75
+ }
76
+
77
+ /**
78
+ * SEO data structure for API responses (flattened format)
79
+ */
80
+ interface SEODataResponse {
81
+ postId: number;
82
+ plugin: "yoast" | "rankmath" | "seopress" | "none";
83
+ title: string | null;
84
+ description: string | null;
85
+ canonical: string | null;
86
+ focusKeyword: string | null;
87
+ openGraph: {
88
+ title: string | null;
89
+ description: string | null;
90
+ };
91
+ twitter: {
92
+ title: string | null;
93
+ description: string | null;
94
+ };
95
+ schema?: SchemaMarkup | undefined;
96
+ raw: Record<string, unknown>;
97
+ lastModified: string;
98
+ success?: boolean;
99
+ message?: string;
100
+ }
101
+
102
+ /**
103
+ * Bulk SEO operation parameters
104
+ */
105
+ interface BulkSEOParams {
106
+ postIds: number[];
107
+ operation: "get" | "update";
108
+ metadata?: Partial<SEOMetadata>;
109
+ batchSize?: number;
110
+ progressCallback?: (processed: number, total: number) => void;
111
+ }
112
+
113
+ /**
114
+ * Extended WordPress client with SEO capabilities
115
+ */
116
+ export class SEOWordPressClient extends WordPressClient {
117
+ private logger = LoggerFactory.tool("seo_client");
118
+ private detectedPlugin: "yoast" | "rankmath" | "seopress" | "none" = "none";
119
+ private pluginFields: SEOPluginFields;
120
+
121
+ constructor(config?: Partial<WordPressClientConfig>) {
122
+ super(config);
123
+
124
+ // Define field mappings for different SEO plugins
125
+ this.pluginFields = {
126
+ yoast: {
127
+ title: "_yoast_wpseo_title",
128
+ description: "_yoast_wpseo_metadesc",
129
+ focusKeyword: "_yoast_wpseo_focuskw",
130
+ canonical: "_yoast_wpseo_canonical",
131
+ noindex: "_yoast_wpseo_meta-robots-noindex",
132
+ nofollow: "_yoast_wpseo_meta-robots-nofollow",
133
+ schema: "_yoast_wpseo_schema_page_type",
134
+ },
135
+ rankmath: {
136
+ title: "rank_math_title",
137
+ description: "rank_math_description",
138
+ focusKeyword: "rank_math_focus_keyword",
139
+ canonical: "rank_math_canonical_url",
140
+ robots: "rank_math_robots",
141
+ schema: "rank_math_rich_snippet",
142
+ },
143
+ seopress: {
144
+ title: "_seopress_titles_title",
145
+ description: "_seopress_titles_desc",
146
+ targetKeyword: "_seopress_analysis_target_kw",
147
+ canonical: "_seopress_robots_canonical",
148
+ robots: "_seopress_robots_index",
149
+ },
150
+ none: {
151
+ title: "meta_title",
152
+ description: "meta_description",
153
+ focusKeyword: "focus_keyword",
154
+ canonical: "canonical_url",
155
+ noindex: "robots_noindex",
156
+ nofollow: "robots_nofollow",
157
+ schema: "schema_markup",
158
+ },
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Initialize and detect SEO plugins
164
+ */
165
+ async initializeSEO(): Promise<void> {
166
+ this.logger.debug("Initializing SEO client and detecting plugins");
167
+
168
+ try {
169
+ // Detect active SEO plugins
170
+ await this.detectSEOPlugins();
171
+
172
+ this.logger.info("SEO client initialized", {
173
+ detectedPlugin: this.detectedPlugin,
174
+ });
175
+ } catch (_error) {
176
+ this.logger.warn("SEO plugin detection failed, using generic approach", {
177
+ _error: _error instanceof Error ? _error.message : String(_error),
178
+ });
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Detect active SEO plugins on the WordPress site
184
+ */
185
+ private async detectSEOPlugins(): Promise<void> {
186
+ try {
187
+ // Get list of installed plugins
188
+ const plugins = await this.get("/wp/v2/plugins");
189
+
190
+ if (!Array.isArray(plugins)) {
191
+ this.logger.debug("Could not retrieve plugins list");
192
+ return;
193
+ }
194
+
195
+ // Check for active SEO plugins in order of preference
196
+ const activePlugins = (plugins as WordPressPlugin[]).filter((plugin) => plugin.status === "active");
197
+
198
+ // Check for Yoast SEO
199
+ if (activePlugins.some((plugin) => plugin.slug === "wordpress-seo")) {
200
+ this.detectedPlugin = "yoast";
201
+ return;
202
+ }
203
+
204
+ // Check for RankMath
205
+ if (activePlugins.some((plugin) => plugin.slug === "seo-by-rank-math")) {
206
+ this.detectedPlugin = "rankmath";
207
+ return;
208
+ }
209
+
210
+ // Check for SEOPress
211
+ if (activePlugins.some((plugin) => plugin.slug === "wp-seopress")) {
212
+ this.detectedPlugin = "seopress";
213
+ return;
214
+ }
215
+
216
+ this.logger.debug("No SEO plugins detected, using generic metadata approach");
217
+ } catch (_error) {
218
+ this.logger.error("Plugin detection failed", {
219
+ _error: _error instanceof Error ? _error.message : String(_error),
220
+ });
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get SEO metadata for a post or page
226
+ */
227
+ async getSEOMetadata(postId: number, type: "post" | "page" = "post"): Promise<SEODataResponse> {
228
+ this.logger.debug("Fetching SEO metadata", { postId, type, plugin: this.detectedPlugin });
229
+
230
+ try {
231
+ // Get the post/page with meta fields
232
+ const content =
233
+ type === "post"
234
+ ? await this.getPost(postId, "edit") // edit context includes meta
235
+ : await this.getPage(postId, "edit");
236
+
237
+ if (!content) {
238
+ throw new Error(`${type} with ID ${postId} not found`);
239
+ }
240
+
241
+ // Extract SEO metadata based on detected plugin
242
+ const metadata = this.extractSEOMetadata(content);
243
+
244
+ // Extract schema markup if available
245
+ const schema = this.extractSchemaMarkup(content);
246
+
247
+ // Get raw plugin data for debugging
248
+ const raw = this.getRawPluginData(content);
249
+
250
+ // For malformed plugin data (detected plugin but no actual data), return null for title/description
251
+ const pluginDetected = this.detectedPlugin !== "none";
252
+ const pluginDataMalformed = pluginDetected && !this.hasPluginData(content);
253
+
254
+ return {
255
+ postId: postId,
256
+ plugin: this.detectedPlugin,
257
+ title: pluginDataMalformed ? null : metadata.title,
258
+ description: pluginDataMalformed ? null : metadata.description,
259
+ canonical: metadata.canonical || null,
260
+ focusKeyword: metadata.focusKeyword || null,
261
+ openGraph: metadata.openGraph || { title: null, description: null },
262
+ twitter: metadata.twitterCard ? {
263
+ title: metadata.twitterCard.title || null,
264
+ description: metadata.twitterCard.description || null
265
+ } : { title: null, description: null },
266
+ schema,
267
+ raw,
268
+ lastModified: content.modified || content.date || new Date().toISOString(),
269
+ };
270
+ } catch (_error) {
271
+ handleToolError(_error, "get SEO metadata", { postId, type });
272
+ throw _error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Update SEO metadata for a post or page
278
+ */
279
+ async updateSEOMetadata(
280
+ postId: number,
281
+ metadata: Partial<SEOMetadata>,
282
+ type: "post" | "page" = "post",
283
+ ): Promise<SEODataResponse> {
284
+ this.logger.debug("Updating SEO metadata", { postId, type, plugin: this.detectedPlugin });
285
+
286
+ try {
287
+ // Check if SEO plugin is available
288
+ if (this.detectedPlugin === "none") {
289
+ return {
290
+ success: false,
291
+ message: "No SEO plugin detected. Cannot update SEO metadata.",
292
+ postId,
293
+ plugin: this.detectedPlugin,
294
+ title: null,
295
+ description: null,
296
+ focusKeyword: null,
297
+ canonical: null,
298
+ openGraph: { title: null, description: null },
299
+ twitter: { title: null, description: null },
300
+ schema: undefined,
301
+ raw: {},
302
+ lastModified: new Date().toISOString(),
303
+ };
304
+ }
305
+
306
+ // Prepare meta fields based on detected plugin
307
+ const metaFields = this.prepareSEOMetaFields(metadata);
308
+
309
+ // Update the post/page with new meta fields
310
+ const updateData = {
311
+ id: postId,
312
+ meta: metaFields,
313
+ };
314
+
315
+ const _updatedContent = type === "post" ? await this.updatePost(updateData) : await this.updatePage(updateData);
316
+
317
+ // Return updated SEO data
318
+ return await this.getSEOMetadata(postId, type);
319
+ } catch (_error) {
320
+ handleToolError(_error, "update SEO metadata", { postId, type });
321
+ throw _error;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Bulk get SEO metadata for multiple posts
327
+ */
328
+ async bulkGetSEOMetadata(params: BulkSEOParams): Promise<SEODataResponse[]> {
329
+ this.logger.info("Starting bulk SEO metadata retrieval", {
330
+ postCount: params.postIds.length,
331
+ batchSize: params.batchSize || 10,
332
+ });
333
+
334
+ const results: SEODataResponse[] = [];
335
+ const batchSize = params.batchSize || 10;
336
+ const total = params.postIds.length;
337
+
338
+ // Process in batches
339
+ for (let i = 0; i < params.postIds.length; i += batchSize) {
340
+ const batch = params.postIds.slice(i, i + batchSize);
341
+
342
+ const batchPromises = batch.map(async (postId) => {
343
+ try {
344
+ return await this.getSEOMetadata(postId);
345
+ } catch (_error) {
346
+ this.logger.warn("Failed to get SEO metadata for post", {
347
+ postId,
348
+ _error: _error instanceof Error ? _error.message : String(_error),
349
+ });
350
+ return null;
351
+ }
352
+ });
353
+
354
+ const batchResults = await Promise.all(batchPromises);
355
+ results.push(...(batchResults.filter((r) => r !== null) as SEODataResponse[]));
356
+
357
+ // Report progress
358
+ const processed = Math.min(i + batchSize, total);
359
+ if (params.progressCallback) {
360
+ params.progressCallback(processed, total);
361
+ }
362
+
363
+ // Small delay between batches to avoid overwhelming the server
364
+ if (i + batchSize < params.postIds.length) {
365
+ await new Promise((resolve) => setTimeout(resolve, 100));
366
+ }
367
+ }
368
+
369
+ this.logger.info("Bulk SEO metadata retrieval completed", {
370
+ requested: total,
371
+ successful: results.length,
372
+ failed: total - results.length,
373
+ });
374
+
375
+ return results;
376
+ }
377
+
378
+ /**
379
+ * Bulk update SEO metadata for multiple posts
380
+ */
381
+ async bulkUpdateSEOMetadata(params: BulkSEOParams & { metadata: Partial<SEOMetadata> }): Promise<SEODataResponse[]> {
382
+ this.logger.info("Starting bulk SEO metadata update", {
383
+ postCount: params.postIds.length,
384
+ batchSize: params.batchSize || 5, // Smaller batches for updates
385
+ });
386
+
387
+ const results: SEODataResponse[] = [];
388
+ const batchSize = params.batchSize || 5;
389
+ const total = params.postIds.length;
390
+
391
+ // Process in smaller batches for updates
392
+ for (let i = 0; i < params.postIds.length; i += batchSize) {
393
+ const batch = params.postIds.slice(i, i + batchSize);
394
+
395
+ const batchPromises = batch.map(async (postId) => {
396
+ try {
397
+ return await this.updateSEOMetadata(postId, params.metadata);
398
+ } catch (_error) {
399
+ this.logger.warn("Failed to update SEO metadata for post", {
400
+ postId,
401
+ _error: _error instanceof Error ? _error.message : String(_error),
402
+ });
403
+ return null;
404
+ }
405
+ });
406
+
407
+ const batchResults = await Promise.all(batchPromises);
408
+ results.push(...(batchResults.filter((r) => r !== null) as SEODataResponse[]));
409
+
410
+ // Report progress
411
+ const processed = Math.min(i + batchSize, total);
412
+ if (params.progressCallback) {
413
+ params.progressCallback(processed, total);
414
+ }
415
+
416
+ // Longer delay between update batches
417
+ if (i + batchSize < params.postIds.length) {
418
+ await new Promise((resolve) => setTimeout(resolve, 200));
419
+ }
420
+ }
421
+
422
+ this.logger.info("Bulk SEO metadata update completed", {
423
+ requested: total,
424
+ successful: results.length,
425
+ failed: total - results.length,
426
+ });
427
+
428
+ return results;
429
+ }
430
+
431
+ /**
432
+ * Get all posts with SEO metadata for site audit
433
+ */
434
+ async getAllPostsWithSEO(
435
+ params: {
436
+ postTypes?: string[];
437
+ maxPosts?: number;
438
+ includePages?: boolean;
439
+ } = {},
440
+ ): Promise<Array<(WordPressPost | WordPressPage) & { seoData?: SEODataResponse }>> {
441
+ this.logger.debug("Fetching all posts with SEO data for audit", params);
442
+
443
+ const maxPosts = params.maxPosts || 100;
444
+ const results: Array<(WordPressPost | WordPressPage) & { seoData?: SEODataResponse }> = [];
445
+
446
+ try {
447
+ // Get posts
448
+ const posts = await this.getPosts({
449
+ per_page: maxPosts,
450
+ status: ["publish"],
451
+ context: "edit", // Include meta fields
452
+ });
453
+
454
+ // Get pages if requested
455
+ let pages: WordPressPage[] = [];
456
+ if (params.includePages) {
457
+ pages = await this.getPages({
458
+ per_page: Math.max(20, Math.floor(maxPosts / 5)), // 20% allocation for pages
459
+ status: ["publish"],
460
+ context: "edit",
461
+ });
462
+ }
463
+
464
+ // Process posts
465
+ for (const post of posts || []) {
466
+ try {
467
+ const seoData = await this.getSEOMetadata(post.id, "post");
468
+ results.push({ ...post, seoData });
469
+ } catch (_error) {
470
+ // Include post without SEO data if metadata fetch fails
471
+ results.push(post);
472
+ this.logger.debug("Failed to get SEO data for post", { postId: post.id });
473
+ }
474
+ }
475
+
476
+ // Process pages
477
+ for (const page of pages) {
478
+ try {
479
+ const seoData = await this.getSEOMetadata(page.id, "page");
480
+ results.push({ ...page, seoData });
481
+ } catch (_error) {
482
+ // Include page without SEO data if metadata fetch fails
483
+ results.push(page);
484
+ this.logger.debug("Failed to get SEO data for page", { pageId: page.id });
485
+ }
486
+ }
487
+
488
+ this.logger.info("Retrieved posts with SEO data", {
489
+ totalPosts: posts?.length || 0,
490
+ totalPages: pages.length,
491
+ withSEOData: results.filter((r) => r.seoData).length,
492
+ });
493
+
494
+ return results;
495
+ } catch (_error) {
496
+ handleToolError(_error, "get all posts with SEO data", params);
497
+ throw _error;
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Extract SEO metadata from WordPress post/page object
503
+ */
504
+ private extractSEOMetadata(content: WordPressPost | WordPressPage): SEOMetadata {
505
+ const meta = (Array.isArray(content.meta) ? {} : content.meta || {}) as Record<string, unknown>;
506
+ const fields = this.pluginFields[this.detectedPlugin];
507
+
508
+ // Extract basic metadata with plugin-specific field handling
509
+ const focusKeyword = this.getPluginFocusKeyword(meta, fields);
510
+ const canonical = this.extractMetaValue(meta, fields?.canonical);
511
+
512
+ const metadata: SEOMetadata = {
513
+ title: this.extractMetaValue(meta, fields?.title) || content.title?.rendered || "",
514
+ description: this.extractMetaValue(meta, fields?.description) || content.excerpt?.rendered || "",
515
+ ...(focusKeyword && { focusKeyword }),
516
+ ...(canonical && { canonical }),
517
+ };
518
+
519
+ // Extract robots directives based on plugin
520
+ if (this.detectedPlugin === "yoast" && "noindex" in fields && "nofollow" in fields) {
521
+ metadata.robots = {
522
+ index: this.extractMetaValue(meta, fields.noindex) !== "1",
523
+ follow: this.extractMetaValue(meta, fields.nofollow) !== "1",
524
+ };
525
+ } else if (this.detectedPlugin === "rankmath" && "robots" in fields) {
526
+ const robotsValue = this.extractMetaValue(meta, fields.robots) || "";
527
+ metadata.robots = {
528
+ index: !robotsValue.includes("noindex"),
529
+ follow: !robotsValue.includes("nofollow"),
530
+ };
531
+ }
532
+
533
+ // Extract OpenGraph and Twitter data based on plugin
534
+ if (this.detectedPlugin === "yoast" && meta.yoast_head_json) {
535
+ const yoastData = meta.yoast_head_json as Record<string, unknown>;
536
+
537
+ metadata.openGraph = {
538
+ title: (yoastData.og_title as string) || metadata.title,
539
+ description: (yoastData.og_description as string) || metadata.description,
540
+ type: content.type === "page" ? "website" : "article",
541
+ url: content.link,
542
+ };
543
+
544
+ const twitterTitle = yoastData.twitter_title as string;
545
+ const twitterDescription = yoastData.twitter_description as string;
546
+
547
+ metadata.twitterCard = {
548
+ card: "summary",
549
+ ...(twitterTitle && { title: twitterTitle }),
550
+ ...(twitterDescription && { description: twitterDescription }),
551
+ };
552
+ } else {
553
+ // Default OpenGraph data
554
+ metadata.openGraph = {
555
+ title: metadata.title,
556
+ description: metadata.description,
557
+ type: content.type === "page" ? "website" : "article",
558
+ url: content.link,
559
+ };
560
+ }
561
+
562
+ return metadata;
563
+ }
564
+
565
+ /**
566
+ * Extract schema markup from post meta
567
+ */
568
+ private extractSchemaMarkup(content: WordPressPost | WordPressPage): SchemaMarkup | undefined {
569
+ const meta = (Array.isArray(content.meta) ? {} : content.meta || {}) as Record<string, unknown>;
570
+ const fields = this.pluginFields[this.detectedPlugin];
571
+
572
+ const schemaData = this.getPluginSchemaData(meta, fields);
573
+ if (!schemaData) {
574
+ return undefined;
575
+ }
576
+
577
+ try {
578
+ // Try to parse JSON-LD schema
579
+ if (typeof schemaData === "string" && schemaData.startsWith("{")) {
580
+ return JSON.parse(schemaData) as SchemaMarkup;
581
+ }
582
+
583
+ // Handle Yoast schema page types
584
+ if (this.detectedPlugin === "yoast" && schemaData) {
585
+ return {
586
+ "@context": "https://schema.org",
587
+ "@type": schemaData as SchemaType,
588
+ };
589
+ }
590
+
591
+ return undefined;
592
+ } catch {
593
+ return undefined;
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Prepare meta fields for SEO metadata update
599
+ */
600
+ private prepareSEOMetaFields(metadata: Partial<SEOMetadata>): Record<string, unknown> {
601
+ const fields = this.pluginFields[this.detectedPlugin];
602
+ const metaFields: Record<string, unknown> = {};
603
+
604
+ // Map metadata to plugin-specific fields
605
+ if (metadata.title && fields?.title) {
606
+ metaFields[fields.title] = metadata.title;
607
+ }
608
+
609
+ if (metadata.description && fields?.description) {
610
+ metaFields[fields.description] = metadata.description;
611
+ }
612
+
613
+ if (metadata.focusKeyword) {
614
+ const focusKeywordField = this.getPluginFocusKeywordField(fields);
615
+ if (focusKeywordField) {
616
+ metaFields[focusKeywordField] = metadata.focusKeyword;
617
+ }
618
+ }
619
+
620
+ if (metadata.canonical && fields?.canonical) {
621
+ metaFields[fields.canonical] = metadata.canonical;
622
+ }
623
+
624
+ // Handle robots directives
625
+ if (metadata.robots && this.detectedPlugin === "yoast" && "noindex" in fields && "nofollow" in fields) {
626
+ if (fields.noindex) {
627
+ metaFields[fields.noindex] = metadata.robots.index ? "0" : "1";
628
+ }
629
+ if (fields.nofollow) {
630
+ metaFields[fields.nofollow] = metadata.robots.follow ? "0" : "1";
631
+ }
632
+ } else if (metadata.robots && this.detectedPlugin === "rankmath" && "robots" in fields) {
633
+ const robotsArray: string[] = [];
634
+ if (!metadata.robots.index) robotsArray.push("noindex");
635
+ if (!metadata.robots.follow) robotsArray.push("nofollow");
636
+ metaFields[fields.robots] = robotsArray.join(",");
637
+ }
638
+
639
+ return metaFields;
640
+ }
641
+
642
+ /**
643
+ * Extract meta value with array handling
644
+ */
645
+ private extractMetaValue(meta: Record<string, unknown>, fieldName?: string): string | null {
646
+ if (!fieldName) {
647
+ return null;
648
+ }
649
+
650
+ // For Yoast, check yoast_head_json first
651
+ if (this.detectedPlugin === "yoast" && meta.yoast_head_json) {
652
+ const yoastData = meta.yoast_head_json as Record<string, unknown>;
653
+
654
+ // Map field names to yoast_head_json properties
655
+ const yoastFieldMap: Record<string, string> = {
656
+ '_yoast_wpseo_title': 'title',
657
+ '_yoast_wpseo_metadesc': 'description',
658
+ '_yoast_wpseo_canonical': 'canonical',
659
+ '_yoast_wpseo_focuskw': 'focuskw'
660
+ };
661
+
662
+ const yoastField = yoastFieldMap[fieldName];
663
+ if (yoastField && yoastData[yoastField]) {
664
+ return yoastData[yoastField] as string;
665
+ }
666
+ }
667
+
668
+ // Fallback to direct meta field lookup
669
+ if (!meta[fieldName]) {
670
+ return null;
671
+ }
672
+
673
+ const value = meta[fieldName];
674
+
675
+ // WordPress meta values can be arrays
676
+ if (Array.isArray(value)) {
677
+ return (value[0] as string) || null;
678
+ }
679
+
680
+ return (value as string) || null;
681
+ }
682
+
683
+ /**
684
+ * Get plugin-specific focus keyword field
685
+ */
686
+ private getPluginFocusKeyword(meta: Record<string, unknown>, fields: Record<string, string>): string | undefined {
687
+ if (this.detectedPlugin === "seopress" && "targetKeyword" in fields) {
688
+ return this.extractMetaValue(meta, fields.targetKeyword) || undefined;
689
+ } else if ("focusKeyword" in fields) {
690
+ return this.extractMetaValue(meta, fields.focusKeyword) || undefined;
691
+ }
692
+ return undefined;
693
+ }
694
+
695
+ /**
696
+ * Get plugin-specific schema data field
697
+ */
698
+ private getPluginSchemaData(meta: Record<string, unknown>, fields: Record<string, string>): string | undefined {
699
+ if ("schema" in fields) {
700
+ return this.extractMetaValue(meta, fields.schema) || undefined;
701
+ }
702
+ return undefined;
703
+ }
704
+
705
+ /**
706
+ * Get plugin-specific focus keyword field name
707
+ */
708
+ private getPluginFocusKeywordField(fields: Record<string, string>): string | undefined {
709
+ if (this.detectedPlugin === "seopress" && "targetKeyword" in fields) {
710
+ return fields.targetKeyword;
711
+ } else if ("focusKeyword" in fields) {
712
+ return fields.focusKeyword;
713
+ }
714
+ return undefined;
715
+ }
716
+
717
+ /**
718
+ * Test SEO plugin integration
719
+ */
720
+ async testSEOIntegration(): Promise<{
721
+ pluginDetected: string;
722
+ canReadMetadata: boolean;
723
+ canWriteMetadata: boolean;
724
+ samplePostsWithSEO: number;
725
+ errors?: string[];
726
+ }> {
727
+ this.logger.info("Testing SEO integration");
728
+
729
+ const result = {
730
+ pluginDetected: this.detectedPlugin,
731
+ canReadMetadata: false,
732
+ canWriteMetadata: false,
733
+ samplePostsWithSEO: 0,
734
+ errors: [] as string[],
735
+ };
736
+
737
+ try {
738
+ // Test reading metadata from recent posts
739
+ const testPosts = await this.getPosts({ per_page: 5, status: ["publish"] });
740
+
741
+ if (testPosts && testPosts.length > 0) {
742
+ let postsWithSEO = 0;
743
+
744
+ for (const post of testPosts) {
745
+ try {
746
+ const seoData = await this.getSEOMetadata(post.id);
747
+ if (seoData.title || seoData.description) {
748
+ postsWithSEO++;
749
+ }
750
+ } catch (_error) {
751
+ result.errors?.push(`Failed to read SEO data for post ${post.id}: ${(_error as Error).message}`);
752
+ }
753
+ }
754
+
755
+ result.samplePostsWithSEO = postsWithSEO;
756
+ result.canReadMetadata = true;
757
+ }
758
+
759
+ // Test writing metadata (if we have posts to test with)
760
+ if (testPosts && testPosts.length > 0 && result.canReadMetadata) {
761
+ try {
762
+ const testPost = testPosts[0];
763
+ const originalSEO = await this.getSEOMetadata(testPost.id);
764
+
765
+ // Make a small test update
766
+ const testMetadata: Partial<SEOMetadata> = {
767
+ description: (originalSEO.description || "") + " [TEST]",
768
+ };
769
+
770
+ await this.updateSEOMetadata(testPost.id, testMetadata);
771
+
772
+ // Restore original data
773
+ await this.updateSEOMetadata(testPost.id, {
774
+ description: originalSEO.description || "",
775
+ });
776
+
777
+ result.canWriteMetadata = true;
778
+ } catch (_error) {
779
+ result.errors?.push(`Failed to write SEO data: ${(_error as Error).message}`);
780
+ }
781
+ }
782
+
783
+ this.logger.info("SEO integration test completed", result);
784
+ return result;
785
+ } catch (_error) {
786
+ result.errors?.push(`SEO integration test failed: ${(_error as Error).message}`);
787
+ return result;
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Get integration status for SEO features
793
+ */
794
+ getIntegrationStatus(): {
795
+ hasPlugin: boolean;
796
+ plugin: string;
797
+ canReadMetadata: boolean;
798
+ canWriteMetadata: boolean;
799
+ features: {
800
+ metaTags: boolean;
801
+ schema: boolean;
802
+ socialMedia: boolean;
803
+ xmlSitemap: boolean;
804
+ breadcrumbs: boolean;
805
+ };
806
+ } {
807
+ const hasPlugin = this.detectedPlugin !== "none";
808
+
809
+ return {
810
+ hasPlugin,
811
+ plugin: this.detectedPlugin,
812
+ canReadMetadata: hasPlugin,
813
+ canWriteMetadata: hasPlugin,
814
+ features: {
815
+ metaTags: hasPlugin,
816
+ schema: hasPlugin,
817
+ socialMedia: hasPlugin,
818
+ xmlSitemap: false, // Would need additional API calls
819
+ breadcrumbs: false,
820
+ },
821
+ };
822
+ }
823
+
824
+ /**
825
+ * Check if content has plugin-specific SEO data
826
+ */
827
+ private hasPluginData(content: WordPressPost | WordPressPage): boolean {
828
+ const meta = (Array.isArray(content.meta) ? {} : content.meta || {}) as Record<string, unknown>;
829
+
830
+ switch (this.detectedPlugin) {
831
+ case "yoast":
832
+ // Check for yoast_head_json or individual meta fields
833
+ return (meta.yoast_head_json !== null && meta.yoast_head_json !== undefined) ||
834
+ (meta._yoast_wpseo_title !== null && meta._yoast_wpseo_title !== undefined);
835
+ case "rankmath":
836
+ return meta.rank_math_title !== null && meta.rank_math_title !== undefined;
837
+ case "seopress":
838
+ return meta._seopress_titles_title !== null && meta._seopress_titles_title !== undefined;
839
+ default:
840
+ return false;
841
+ }
842
+ }
843
+
844
+ /**
845
+ * Get raw plugin data for debugging purposes
846
+ */
847
+ private getRawPluginData(content: WordPressPost | WordPressPage): Record<string, unknown> {
848
+ const meta = (Array.isArray(content.meta) ? {} : content.meta || {}) as Record<string, unknown>;
849
+
850
+ // Return the raw plugin-specific data based on detected plugin
851
+ switch (this.detectedPlugin) {
852
+ case "yoast":
853
+ return (meta.yoast_head_json as Record<string, unknown>) || {};
854
+ case "rankmath":
855
+ // Extract RankMath specific fields
856
+ const rankMathData: Record<string, unknown> = {};
857
+ Object.keys(meta).forEach(key => {
858
+ if (key.startsWith('rank_math_')) {
859
+ rankMathData[key] = meta[key];
860
+ }
861
+ });
862
+ return rankMathData;
863
+ case "seopress":
864
+ // Extract SEOPress specific fields
865
+ const seopressData: Record<string, unknown> = {};
866
+ Object.keys(meta).forEach(key => {
867
+ if (key.startsWith('_seopress_')) {
868
+ seopressData[key] = meta[key];
869
+ }
870
+ });
871
+ return seopressData;
872
+ default:
873
+ return {};
874
+ }
875
+ }
876
+ }