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,694 @@
1
+ /**
2
+ * SEO Metadata Generator
3
+ *
4
+ * Generates optimized SEO metadata including title tags, meta descriptions,
5
+ * OpenGraph tags, and Twitter Card metadata with AI assistance and safety filters.
6
+ *
7
+ * @since 2.7.0
8
+ */
9
+
10
+ import { LoggerFactory } from "../../../utils/logger.js";
11
+ import { Config } from "../../../config/Config.js";
12
+ import type { WordPressPost } from "../../../types/wordpress.js";
13
+ import type { SEOMetadata, SEOToolParams } from "../../../types/seo.js";
14
+
15
+ /**
16
+ * Content safety filters and validation rules
17
+ */
18
+ interface SafetyFilters {
19
+ maxTitleLength: number;
20
+ minDescriptionLength: number;
21
+ maxDescriptionLength: number;
22
+ forbiddenWords: string[];
23
+ requiredFields: string[];
24
+ }
25
+
26
+ /**
27
+ * Metadata generation options
28
+ */
29
+ interface MetaGenerationOptions {
30
+ includeKeywords?: boolean;
31
+ brandVoice?: "professional" | "casual" | "technical" | "friendly";
32
+ targetAudience?: "general" | "technical" | "beginner" | "expert";
33
+ includeCallToAction?: boolean;
34
+ preserveExisting?: boolean;
35
+ }
36
+
37
+ /**
38
+ * SEO metadata generator with AI assistance and safety validation
39
+ */
40
+ export class MetaGenerator {
41
+ private logger = LoggerFactory.tool("meta_generator");
42
+ private config = Config.getInstance().get().seo;
43
+
44
+ private readonly safetyFilters: SafetyFilters = {
45
+ maxTitleLength: this.config.metadata.titleMaxLength || 60,
46
+ minDescriptionLength: this.config.metadata.descriptionMinLength || 155,
47
+ maxDescriptionLength: this.config.metadata.descriptionMaxLength || 160,
48
+ forbiddenWords: [
49
+ "spam",
50
+ "scam",
51
+ "fake",
52
+ "clickbait",
53
+ "hack",
54
+ "cheat",
55
+ "guaranteed",
56
+ "instant",
57
+ "miracle",
58
+ "secret",
59
+ ],
60
+ requiredFields: ["title", "description"],
61
+ };
62
+
63
+ /**
64
+ * Generate comprehensive SEO metadata for a WordPress post
65
+ *
66
+ * @param post - WordPress post data
67
+ * @param params - Generation parameters
68
+ * @param options - Generation options
69
+ * @returns Optimized SEO metadata
70
+ */
71
+ async generateMetadata(
72
+ post: WordPressPost,
73
+ params: SEOToolParams,
74
+ options: MetaGenerationOptions = {},
75
+ ): Promise<SEOMetadata> {
76
+ const siteLogger = LoggerFactory.tool("seo_generate_metadata", params.site);
77
+
78
+ return await siteLogger.time("Generate SEO metadata", async () => {
79
+ // Extract content for analysis
80
+ const content = this.extractPostContent(post);
81
+ const focusKeyword = params.focusKeywords?.[0] || "";
82
+
83
+ // Generate title tag
84
+ const title = await this.generateTitle(post, content, focusKeyword, options);
85
+
86
+ // Generate meta description
87
+ const description = await this.generateDescription(post, content, focusKeyword, options);
88
+
89
+ // Generate OpenGraph metadata
90
+ const openGraph = await this.generateOpenGraph(post, title, description, options);
91
+
92
+ // Generate Twitter Card metadata
93
+ const twitterCard = await this.generateTwitterCard(post, title, description, options);
94
+
95
+ // Create canonical URL
96
+ const canonical = this.generateCanonicalUrl(post);
97
+
98
+ // Set robots directives
99
+ const robots = this.generateRobotsDirectives(post, options);
100
+
101
+ const metadata: SEOMetadata = {
102
+ title,
103
+ description,
104
+ };
105
+
106
+ if (focusKeyword) {
107
+ metadata.focusKeyword = focusKeyword;
108
+ }
109
+
110
+ if (canonical) {
111
+ metadata.canonical = canonical;
112
+ }
113
+
114
+ if (robots) {
115
+ metadata.robots = robots;
116
+ }
117
+
118
+ if (openGraph) {
119
+ metadata.openGraph = openGraph;
120
+ }
121
+
122
+ if (twitterCard) {
123
+ metadata.twitterCard = twitterCard;
124
+ }
125
+
126
+ // Apply safety filters
127
+ this.applySafetyFilters(metadata);
128
+
129
+ // Re-optimize description length after safety filters may have shortened it
130
+ metadata.description = this.optimizeDescriptionLength(metadata.description);
131
+
132
+ // Validate required fields
133
+ this.validateMetadata(metadata);
134
+
135
+ siteLogger.info("Generated SEO metadata", {
136
+ titleLength: title.length,
137
+ descriptionLength: description.length,
138
+ hasOpenGraph: Boolean(openGraph),
139
+ hasTwitterCard: Boolean(twitterCard),
140
+ focusKeyword,
141
+ });
142
+
143
+ return metadata;
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Generate optimized title tag
149
+ *
150
+ * @param post - WordPress post
151
+ * @param content - Extracted content
152
+ * @param focusKeyword - Primary keyword
153
+ * @param options - Generation options
154
+ * @returns Optimized title
155
+ * @private
156
+ */
157
+ private async generateTitle(
158
+ post: WordPressPost,
159
+ content: string,
160
+ focusKeyword: string,
161
+ options: MetaGenerationOptions,
162
+ ): Promise<string> {
163
+ const originalTitle = typeof post.title === "object" ? post.title.rendered : post.title || "";
164
+
165
+ // Check if title is completely empty - this should be an error
166
+ if (!originalTitle || originalTitle.trim() === "") {
167
+ throw new Error("Title is required and cannot be empty");
168
+ }
169
+
170
+ // If preserving existing and title exists, return it if it's good
171
+ if (options.preserveExisting && originalTitle && originalTitle.length <= this.safetyFilters.maxTitleLength) {
172
+ return originalTitle;
173
+ }
174
+
175
+ // Generate optimized title
176
+ let optimizedTitle = originalTitle;
177
+
178
+ // Truncate first if too long, then add focus keyword if needed
179
+ if (optimizedTitle.length > this.safetyFilters.maxTitleLength) {
180
+ optimizedTitle = this.truncateTitle(optimizedTitle, focusKeyword);
181
+ }
182
+
183
+ // Add focus keyword if not present and specified, and there's room
184
+ if (focusKeyword && !optimizedTitle.toLowerCase().includes(focusKeyword.toLowerCase())) {
185
+ const withKeyword = `${focusKeyword}: ${optimizedTitle}`;
186
+ if (withKeyword.length <= this.safetyFilters.maxTitleLength) {
187
+ optimizedTitle = withKeyword;
188
+ } else {
189
+ // Try to fit keyword by shortening original title
190
+ const availableSpace = this.safetyFilters.maxTitleLength - focusKeyword.length - 2; // 2 for ": "
191
+ if (availableSpace > 10) {
192
+ // Minimum meaningful title length
193
+ const shortened = optimizedTitle.substring(0, availableSpace - 3) + "...";
194
+ optimizedTitle = `${focusKeyword}: ${shortened}`;
195
+ }
196
+ }
197
+ }
198
+
199
+ // Apply brand voice modifications
200
+ optimizedTitle = this.applyBrandVoice(optimizedTitle, options.brandVoice || "professional");
201
+
202
+ return optimizedTitle;
203
+ }
204
+
205
+ /**
206
+ * Generate meta description
207
+ *
208
+ * @param post - WordPress post
209
+ * @param content - Extracted content
210
+ * @param focusKeyword - Primary keyword
211
+ * @param options - Generation options
212
+ * @returns Optimized description
213
+ * @private
214
+ */
215
+ private async generateDescription(
216
+ post: WordPressPost,
217
+ content: string,
218
+ focusKeyword: string,
219
+ options: MetaGenerationOptions,
220
+ ): Promise<string> {
221
+ const excerpt = typeof post.excerpt === "object" ? post.excerpt.rendered : post.excerpt || "";
222
+ const plainContent = this.stripHtml(content);
223
+
224
+ // Use excerpt as starting point if available
225
+ let description = excerpt || this.extractFirstSentences(plainContent, 2);
226
+
227
+ // Ensure focus keyword is included
228
+ if (focusKeyword && !description.toLowerCase().includes(focusKeyword.toLowerCase())) {
229
+ description = this.insertKeywordNaturally(description, focusKeyword);
230
+ }
231
+
232
+ // Apply brand voice first (before length optimization)
233
+ description = this.applyBrandVoice(description, options.brandVoice || "professional");
234
+
235
+ // Add call to action if requested (before length optimization)
236
+ if (options.includeCallToAction) {
237
+ description = this.addCallToAction(description, options.brandVoice || "professional");
238
+ }
239
+
240
+ // Ensure proper length (this should be the final step)
241
+ description = this.optimizeDescriptionLength(description);
242
+
243
+ return description;
244
+ }
245
+
246
+ /**
247
+ * Generate OpenGraph metadata
248
+ *
249
+ * @param post - WordPress post
250
+ * @param title - Optimized title
251
+ * @param description - Optimized description
252
+ * @param options - Generation options
253
+ * @returns OpenGraph metadata
254
+ * @private
255
+ */
256
+ private async generateOpenGraph(
257
+ post: WordPressPost,
258
+ title: string,
259
+ description: string,
260
+ options: MetaGenerationOptions,
261
+ ): Promise<SEOMetadata["openGraph"]> {
262
+ const featuredImage = this.getFeaturedImageUrl(post);
263
+
264
+ const openGraph: SEOMetadata["openGraph"] = {
265
+ title,
266
+ description,
267
+ type: this.determineOpenGraphType(post),
268
+ url: post.link,
269
+ siteName: this.getSiteName(),
270
+ locale: "en_US", // Could be made configurable
271
+ };
272
+
273
+ if (featuredImage) {
274
+ openGraph.image = featuredImage;
275
+ }
276
+
277
+ return openGraph;
278
+ }
279
+
280
+ /**
281
+ * Generate Twitter Card metadata
282
+ *
283
+ * @param post - WordPress post
284
+ * @param title - Optimized title
285
+ * @param description - Optimized description
286
+ * @param options - Generation options
287
+ * @returns Twitter Card metadata
288
+ * @private
289
+ */
290
+ private async generateTwitterCard(
291
+ post: WordPressPost,
292
+ title: string,
293
+ description: string,
294
+ options: MetaGenerationOptions,
295
+ ): Promise<SEOMetadata["twitterCard"]> {
296
+ const featuredImage = this.getFeaturedImageUrl(post);
297
+ const twitterHandle = this.getTwitterHandle();
298
+ const hasImage = Boolean(featuredImage);
299
+
300
+ const twitterCard: SEOMetadata["twitterCard"] = {
301
+ card: hasImage ? "summary_large_image" : "summary",
302
+ title,
303
+ description,
304
+ };
305
+
306
+ if (featuredImage) {
307
+ twitterCard.image = featuredImage;
308
+ }
309
+
310
+ if (twitterHandle) {
311
+ twitterCard.site = twitterHandle;
312
+ twitterCard.creator = twitterHandle;
313
+ }
314
+
315
+ return twitterCard;
316
+ }
317
+
318
+ /**
319
+ * Generate canonical URL
320
+ *
321
+ * @param post - WordPress post
322
+ * @returns Canonical URL
323
+ * @private
324
+ */
325
+ private generateCanonicalUrl(post: WordPressPost): string {
326
+ // Use the post's permalink, ensuring HTTPS
327
+ return post.link?.replace(/^http:/, "https:") || "";
328
+ }
329
+
330
+ /**
331
+ * Generate robots meta directives
332
+ *
333
+ * @param post - WordPress post
334
+ * @param options - Generation options
335
+ * @returns Robots directives
336
+ * @private
337
+ */
338
+ private generateRobotsDirectives(post: WordPressPost, options: MetaGenerationOptions): SEOMetadata["robots"] {
339
+ const shouldIndex = post.status === "publish";
340
+
341
+ return {
342
+ index: shouldIndex,
343
+ follow: true,
344
+ archive: shouldIndex,
345
+ snippet: true,
346
+ imageindex: true,
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Apply safety filters to metadata
352
+ *
353
+ * @param metadata - Metadata to filter
354
+ * @private
355
+ */
356
+ private applySafetyFilters(metadata: SEOMetadata): void {
357
+ // Check for forbidden words
358
+ const forbiddenRegex = new RegExp(this.safetyFilters.forbiddenWords.join("|"), "gi");
359
+
360
+ if (forbiddenRegex.test(metadata.title)) {
361
+ this.logger.warn("Forbidden words detected in title", { title: metadata.title });
362
+ // Replace forbidden words with alternatives or remove them
363
+ metadata.title = metadata.title.replace(forbiddenRegex, (match) => {
364
+ return this.getSafeAlternative(match.toLowerCase());
365
+ });
366
+ }
367
+
368
+ if (forbiddenRegex.test(metadata.description)) {
369
+ this.logger.warn("Forbidden words detected in description", {
370
+ description: metadata.description.substring(0, 50),
371
+ });
372
+ metadata.description = metadata.description.replace(forbiddenRegex, (match) => {
373
+ return this.getSafeAlternative(match.toLowerCase());
374
+ });
375
+ }
376
+
377
+ // Remove any potential script tags or HTML
378
+ metadata.title = this.sanitizeText(metadata.title);
379
+ metadata.description = this.sanitizeText(metadata.description);
380
+ }
381
+
382
+ /**
383
+ * Validate metadata meets requirements
384
+ *
385
+ * @param metadata - Metadata to validate
386
+ * @private
387
+ */
388
+ private validateMetadata(metadata: SEOMetadata): void {
389
+ if (!metadata.title || metadata.title.length === 0) {
390
+ throw new Error("Title is required for SEO metadata");
391
+ }
392
+
393
+ if (metadata.title.length > this.safetyFilters.maxTitleLength) {
394
+ throw new Error(`Title exceeds maximum length of ${this.safetyFilters.maxTitleLength} characters`);
395
+ }
396
+
397
+ if (!metadata.description || metadata.description.length === 0) {
398
+ throw new Error("Description is required for SEO metadata");
399
+ }
400
+
401
+ if (metadata.description.length < this.safetyFilters.minDescriptionLength) {
402
+ throw new Error(`Description must be at least ${this.safetyFilters.minDescriptionLength} characters`);
403
+ }
404
+
405
+ if (metadata.description.length > this.safetyFilters.maxDescriptionLength) {
406
+ throw new Error(`Description exceeds maximum length of ${this.safetyFilters.maxDescriptionLength} characters`);
407
+ }
408
+ }
409
+
410
+ // Helper methods
411
+
412
+ private extractPostContent(post: WordPressPost): string {
413
+ const titleContent = typeof post.title === "object" ? post.title.rendered : post.title || "";
414
+ const bodyContent = typeof post.content === "object" ? post.content.rendered : post.content || "";
415
+ const excerptContent = typeof post.excerpt === "object" ? post.excerpt.rendered : post.excerpt || "";
416
+
417
+ return `${titleContent} ${bodyContent} ${excerptContent}`.trim();
418
+ }
419
+
420
+ private stripHtml(html: string): string {
421
+ return html
422
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
423
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
424
+ .replace(/<[^>]*>/g, "")
425
+ .replace(/&nbsp;/g, " ")
426
+ .replace(/&[#\w]+;/g, "")
427
+ .replace(/\s+/g, " ")
428
+ .trim();
429
+ }
430
+
431
+ private extractFirstSentences(text: string, count: number): string {
432
+ const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
433
+ const result = sentences.slice(0, count).join(". ");
434
+ // Always add a period at the end if there's content
435
+ return result ? result + "." : "";
436
+ }
437
+
438
+ private insertKeywordNaturally(text: string, keyword: string): string {
439
+ // Simple implementation - could be enhanced with NLP
440
+ if (text.length < 50) {
441
+ return `${keyword}: ${text}`;
442
+ }
443
+
444
+ // Insert after first sentence
445
+ const sentences = text.split(". ");
446
+ if (sentences.length > 1) {
447
+ sentences[0] += ` ${keyword}`;
448
+ return sentences.join(". ");
449
+ }
450
+
451
+ return `${text} Learn about ${keyword}.`;
452
+ }
453
+
454
+ private addCallToAction(description: string, brandVoice: string): string {
455
+ const ctas = {
456
+ professional: "Learn more today.",
457
+ casual: "Check it out!",
458
+ technical: "Read the full guide.",
459
+ friendly: "Discover more!",
460
+ };
461
+
462
+ const cta = ctas[brandVoice as keyof typeof ctas] || ctas.professional;
463
+
464
+ // Add CTA if there's room
465
+ if (description.length + cta.length + 1 <= this.safetyFilters.maxDescriptionLength) {
466
+ return `${description} ${cta}`;
467
+ }
468
+
469
+ return description;
470
+ }
471
+
472
+ private optimizeDescriptionLength(description: string): string {
473
+ const minLength = this.safetyFilters.minDescriptionLength;
474
+ const maxLength = this.safetyFilters.maxDescriptionLength;
475
+
476
+ let result = description.trim();
477
+
478
+ // If already perfect length, return as is
479
+ if (result.length >= minLength && result.length <= maxLength) {
480
+ return result;
481
+ }
482
+
483
+ // If too long, truncate with word boundary
484
+ if (result.length > maxLength) {
485
+ const truncated = result.substring(0, maxLength - 3);
486
+ const lastSpace = truncated.lastIndexOf(" ");
487
+ return (lastSpace > 0 ? truncated.substring(0, lastSpace) : truncated).trim() + "...";
488
+ }
489
+
490
+ // If too short, extend systematically to reach exactly minLength
491
+ if (result.length < minLength) {
492
+ const extensions = [
493
+ " This comprehensive guide provides detailed insights and practical tips for better results.",
494
+ " Learn more about best practices and implementation strategies.",
495
+ " Discover effective techniques and proven methods.",
496
+ " Get expert advice and actionable recommendations.",
497
+ " Additional content for completeness and better SEO optimization.",
498
+ ];
499
+
500
+ // Add extensions one by one
501
+ for (const extension of extensions) {
502
+ if (result.length >= minLength) break;
503
+
504
+ const needed = minLength - result.length;
505
+ if (result.length + extension.length <= maxLength) {
506
+ // Add full extension
507
+ result += extension;
508
+ } else if (needed <= maxLength - result.length) {
509
+ // Add partial extension to reach exactly minLength
510
+ result += extension.substring(0, needed);
511
+ break;
512
+ }
513
+ }
514
+
515
+ // Final padding if still short (should rarely happen)
516
+ while (result.length < minLength && result.length < maxLength) {
517
+ const needed = minLength - result.length;
518
+ const padding = " Additional content.";
519
+ if (result.length + padding.length <= maxLength) {
520
+ result += padding;
521
+ } else if (needed > 0) {
522
+ result += padding.substring(0, Math.min(needed, maxLength - result.length));
523
+ break;
524
+ } else {
525
+ break;
526
+ }
527
+ }
528
+ }
529
+
530
+ // Final safety check and guarantee minimum length
531
+ if (result.length > maxLength) {
532
+ result = result.substring(0, maxLength);
533
+ }
534
+
535
+ // Absolute final guarantee - ensure we meet minimum length
536
+ if (result.length < minLength) {
537
+ const shortfall = minLength - result.length;
538
+ // Add exactly the needed characters
539
+ for (let i = 0; i < shortfall && result.length < maxLength; i++) {
540
+ result += ".";
541
+ }
542
+ }
543
+
544
+ // One more check to be absolutely sure
545
+ if (result.length < minLength && result.length < maxLength) {
546
+ // This should never happen, but as a final failsafe
547
+ result = result.padEnd(minLength, ".");
548
+ }
549
+
550
+ return result;
551
+ }
552
+
553
+ private truncateTitle(title: string, focusKeyword: string): string {
554
+ const maxLength = this.safetyFilters.maxTitleLength;
555
+
556
+ // If already within length, return as is
557
+ if (title.length <= maxLength) {
558
+ return title;
559
+ }
560
+
561
+ // Simple truncation at word boundary if no focus keyword
562
+ if (!focusKeyword || !title.toLowerCase().includes(focusKeyword.toLowerCase())) {
563
+ const truncated = title.substring(0, maxLength - 3);
564
+ const lastSpace = truncated.lastIndexOf(" ");
565
+ const result = truncated.substring(0, lastSpace > 0 ? lastSpace : truncated.length).trim() + "...";
566
+ return result.length <= maxLength ? result : title.substring(0, maxLength);
567
+ }
568
+
569
+ // Find keyword position
570
+ const keywordIndex = title.toLowerCase().indexOf(focusKeyword.toLowerCase());
571
+ const keywordEnd = keywordIndex + focusKeyword.length;
572
+
573
+ // Strategy: Try to keep keyword and balanced context
574
+ // Available space for context around keyword
575
+ const spaceAroundKeyword = maxLength - focusKeyword.length;
576
+
577
+ // If we need ellipsis on both sides, reserve space for them
578
+ const maxContextLength = spaceAroundKeyword - 6; // Reserve for "..." on both sides
579
+
580
+ if (maxContextLength <= 0) {
581
+ // Not enough space for context, just return truncated keyword
582
+ return focusKeyword.substring(0, maxLength);
583
+ }
584
+
585
+ // Calculate how much context to include before and after
586
+ const beforeAvailable = keywordIndex;
587
+ const afterAvailable = title.length - keywordEnd;
588
+
589
+ let beforeLength = Math.min(beforeAvailable, Math.floor(maxContextLength / 2));
590
+ let afterLength = Math.min(afterAvailable, maxContextLength - beforeLength);
591
+
592
+ // Adjust if we have extra space
593
+ if (beforeLength < Math.floor(maxContextLength / 2) && afterAvailable > afterLength) {
594
+ afterLength = Math.min(afterAvailable, maxContextLength - beforeLength);
595
+ }
596
+ if (afterLength < Math.floor(maxContextLength / 2) && beforeAvailable > beforeLength) {
597
+ beforeLength = Math.min(beforeAvailable, maxContextLength - afterLength);
598
+ }
599
+
600
+ // Build the result
601
+ let result = "";
602
+
603
+ // Add beginning with ellipsis if needed
604
+ if (beforeLength < beforeAvailable) {
605
+ const start = keywordIndex - beforeLength;
606
+ result += "..." + title.substring(start, keywordIndex);
607
+ } else {
608
+ result += title.substring(0, keywordIndex);
609
+ }
610
+
611
+ // Add keyword
612
+ result += title.substring(keywordIndex, keywordEnd);
613
+
614
+ // Add end with ellipsis if needed
615
+ if (afterLength < afterAvailable) {
616
+ result += title.substring(keywordEnd, keywordEnd + afterLength) + "...";
617
+ } else {
618
+ result += title.substring(keywordEnd);
619
+ }
620
+
621
+ // Emergency truncation if still too long
622
+ if (result.length > maxLength) {
623
+ result = result.substring(0, maxLength);
624
+ }
625
+
626
+ return result;
627
+ }
628
+
629
+ private applyBrandVoice(text: string, brandVoice: string): string {
630
+ // Simple brand voice application - could be enhanced with AI
631
+ switch (brandVoice) {
632
+ case "casual":
633
+ return text.replace(/\.$/, "!");
634
+ case "technical":
635
+ // Keep formal tone
636
+ return text;
637
+ case "friendly":
638
+ if (!text.includes("!") && !text.includes("?")) {
639
+ return text.replace(/\.$/, " 😊");
640
+ }
641
+ return text;
642
+ default:
643
+ return text;
644
+ }
645
+ }
646
+
647
+ private determineOpenGraphType(post: WordPressPost): string {
648
+ // Could be enhanced based on post type or content analysis
649
+ return post.type === "page" ? "website" : "article";
650
+ }
651
+
652
+ private getSiteName(): string {
653
+ // This would ideally come from WordPress site settings
654
+ return "WordPress Site";
655
+ }
656
+
657
+ private getFeaturedImageUrl(post: WordPressPost): string | undefined {
658
+ // This would need to be fetched from WordPress media API
659
+ // For now, return undefined
660
+ return undefined;
661
+ }
662
+
663
+ private getTwitterHandle(): string | undefined {
664
+ // This would come from site configuration
665
+ return undefined;
666
+ }
667
+
668
+ private getSafeAlternative(forbiddenWord: string): string {
669
+ const alternatives: Record<string, string> = {
670
+ spam: "unwanted",
671
+ scam: "fraud",
672
+ fake: "artificial",
673
+ clickbait: "attention-grabbing",
674
+ hack: "tip",
675
+ cheat: "shortcut",
676
+ guaranteed: "reliable",
677
+ instant: "quick",
678
+ miracle: "effective",
679
+ secret: "insider",
680
+ };
681
+
682
+ return alternatives[forbiddenWord] || "[filtered]";
683
+ }
684
+
685
+ private sanitizeText(text: string): string {
686
+ return text
687
+ .replace(/<[^>]*>/g, "") // Remove HTML tags
688
+ .replace(/javascript:/gi, "") // Remove javascript: urls
689
+ .replace(/on\w+\s*=/gi, "") // Remove event handlers
690
+ .trim();
691
+ }
692
+ }
693
+
694
+ export default MetaGenerator;