|
| 1 | +/** |
| 2 | + * Semantic keyword cluster definition |
| 3 | + */ |
| 4 | +interface SemanticCluster { |
| 5 | + coreTerms: string[] // Primary concepts (always include if missing) |
| 6 | + synonyms: string[] // Alternative terms (include strategically) |
| 7 | + intentPhrases: string[] // User search phrases (high-value additions) |
| 8 | + modifiers: string[] // Decision-making keywords (selective inclusion) |
| 9 | +} |
| 10 | + |
| 11 | +/** |
| 12 | + * Enhance excerpt with path-based keywords for better SEO. |
| 13 | + * Automatically appends relevant semantic clusters based on URL path patterns. |
| 14 | + * |
| 15 | + * @param originalExcerpt - The original excerpt from frontmatter |
| 16 | + * @param pathname - The current page pathname |
| 17 | + * @returns Enhanced excerpt with comprehensive keyword coverage |
| 18 | + */ |
| 19 | +export function enhanceExcerpt(originalExcerpt: string, pathname: string): string { |
| 20 | + if (!originalExcerpt || !pathname) { |
| 21 | + return originalExcerpt || "" |
| 22 | + } |
| 23 | + |
| 24 | + // Define semantic keyword clusters for different paths |
| 25 | + const pathKeywordClusters = new Map<string, SemanticCluster>([ |
| 26 | + [ |
| 27 | + "/ccip/", |
| 28 | + { |
| 29 | + coreTerms: ["bridge", "token bridge", "cross-chain bridge"], |
| 30 | + synonyms: ["crypto bridge", "asset bridge", "blockchain bridge"], |
| 31 | + intentPhrases: ["bridge tokens", "cross-chain transfer"], |
| 32 | + modifiers: ["secure bridge", "fast bridge", "low fees"], |
| 33 | + }, |
| 34 | + ], |
| 35 | + |
| 36 | + // Future expansions: |
| 37 | + // ["/vrf/", { |
| 38 | + // coreTerms: ["random number generation", "verifiable randomness"], |
| 39 | + // synonyms: ["blockchain randomness", "cryptographic random"], |
| 40 | + // intentPhrases: ["generate random numbers", "secure randomness"], |
| 41 | + // modifiers: ["provably fair", "tamper-proof"] |
| 42 | + // }], |
| 43 | + ]) |
| 44 | + |
| 45 | + let enhancedExcerpt = originalExcerpt |
| 46 | + |
| 47 | + // Apply semantic keyword enhancement |
| 48 | + for (const [pathPrefix, cluster] of pathKeywordClusters) { |
| 49 | + if (pathname.startsWith(pathPrefix)) { |
| 50 | + enhancedExcerpt = applySEOCluster(enhancedExcerpt, cluster, pathname) |
| 51 | + break // Only apply one cluster per path |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + // Clean up and optimize |
| 56 | + return optimizeKeywordString(enhancedExcerpt) |
| 57 | +} |
| 58 | + |
| 59 | +/** |
| 60 | + * Apply semantic cluster to excerpt with intelligent selection |
| 61 | + */ |
| 62 | +function applySEOCluster(excerpt: string, cluster: SemanticCluster, pathname: string): string { |
| 63 | + let enhanced = excerpt |
| 64 | + const lowerExcerpt = excerpt.toLowerCase() |
| 65 | + |
| 66 | + // 1. Always add missing core terms (critical for classification) |
| 67 | + for (const term of cluster.coreTerms) { |
| 68 | + if (!hasKeywordVariant(lowerExcerpt, term)) { |
| 69 | + enhanced = `${enhanced} ${term}` |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + // 2. Add strategic synonyms (avoid keyword stuffing) |
| 74 | + const synonymsToAdd = cluster.synonyms.filter((synonym) => !hasKeywordVariant(lowerExcerpt, synonym)).slice(0, 2) // Limit to 2 synonyms max |
| 75 | + |
| 76 | + for (const synonym of synonymsToAdd) { |
| 77 | + enhanced = `${enhanced} ${synonym}` |
| 78 | + } |
| 79 | + |
| 80 | + // 3. Add high-value intent phrases (if tutorial/guide content) |
| 81 | + if (pathname.includes("/tutorial") || pathname.includes("/guide")) { |
| 82 | + const intentToAdd = cluster.intentPhrases.filter((phrase) => !hasKeywordVariant(lowerExcerpt, phrase)).slice(0, 1) // Limit to 1 intent phrase |
| 83 | + |
| 84 | + for (const intent of intentToAdd) { |
| 85 | + enhanced = `${enhanced} ${intent}` |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + // 4. Add selective modifiers based on content type |
| 90 | + if (shouldAddModifiers(pathname)) { |
| 91 | + const modifierToAdd = cluster.modifiers.find((modifier) => !hasKeywordVariant(lowerExcerpt, modifier)) |
| 92 | + |
| 93 | + if (modifierToAdd) { |
| 94 | + enhanced = `${enhanced} ${modifierToAdd}` |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + return enhanced |
| 99 | +} |
| 100 | + |
| 101 | +/** |
| 102 | + * Check if excerpt contains keyword or its variants |
| 103 | + */ |
| 104 | +function hasKeywordVariant(lowerExcerpt: string, keyword: string): boolean { |
| 105 | + const keywordParts = keyword.toLowerCase().split(" ") |
| 106 | + |
| 107 | + // Check for exact phrase |
| 108 | + if (lowerExcerpt.includes(keyword.toLowerCase())) { |
| 109 | + return true |
| 110 | + } |
| 111 | + |
| 112 | + // Check if all keyword parts exist (different order/spacing) |
| 113 | + return keywordParts.every((part) => lowerExcerpt.includes(part)) |
| 114 | +} |
| 115 | + |
| 116 | +/** |
| 117 | + * Determine if modifiers should be added based on content context |
| 118 | + */ |
| 119 | +function shouldAddModifiers(pathname: string): boolean { |
| 120 | + // Add modifiers for comparison/selection content |
| 121 | + return ( |
| 122 | + pathname.includes("/tutorial") || |
| 123 | + pathname.includes("/comparison") || |
| 124 | + pathname.includes("/guide") || |
| 125 | + pathname.includes("/best-practices") |
| 126 | + ) |
| 127 | +} |
| 128 | + |
| 129 | +/** |
| 130 | + * Optimize the final keyword string for SEO |
| 131 | + */ |
| 132 | +function optimizeKeywordString(keywords: string): string { |
| 133 | + return keywords |
| 134 | + .replace(/\s+/g, " ") // Normalize spaces |
| 135 | + .trim() // Remove leading/trailing spaces |
| 136 | + .substring(0, 500) // Limit total length (SEO best practice) |
| 137 | +} |
0 commit comments