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