/** * Internal Link Configuration for SEO Tool * Defines rules for internal link optimization and analysis */ /** * Priority link configuration * These links will be prioritized when optimizing internal links */ export class PriorityLink { constructor(targetRoute, keywords, maxLinks, anchorTexts, priority) { this.targetRoute = targetRoute this.keywords = keywords this.maxLinks = maxLinks this.anchorTexts = anchorTexts this.priority = priority } } /** * Keyword to page mapping * Auto-magically link specific keywords to target pages */ export class KeywordMapping { constructor(keyword, targetRoute, variations, useNaturalAnchorText) { this.keyword = keyword this.targetRoute = targetRoute this.variations = variations || [] this.useNaturalAnchorText = useNaturalAnchorText || false } } /** * Page-specific configuration */ export class PageConfig { constructor( excludeSections, maxLinksPerPage, minWordsBetweenLinks, preferredAnchorText ) { this.excludeSections = excludeSections || [] this.maxLinksPerPage = maxLinksPerPage this.minWordsBetweenLinks = minWordsBetweenLinks this.preferredAnchorText = preferredAnchorText } } /** * Link density configuration */ export class LinkDensityConfig { constructor( maxLinksPerPage, minWordsBetweenLinks, maxLinksPerParagraph, maxPercentagePerPage ) { this.maxLinksPerPage = maxLinksPerPage this.minWordsBetweenLinks = minWordsBetweenLinks this.maxLinksPerParagraph = maxLinksPerParagraph this.maxPercentagePerPage = maxPercentagePerPage } } /** * SEO scoring weights */ export class ScoringWeights { constructor( title, description, internalLinks, contentLength, keywordOptimization, structure, imageOptimization ) { this.title = title this.description = description this.internalLinks = internalLinks this.contentLength = contentLength this.keywordOptimization = keywordOptimization this.structure = structure this.imageOptimization = imageOptimization } } /** * Master configuration */ export class InternalLinkConfig { constructor( businessName, websiteUrl, priorityLinks, keywordMappings, pageConfigs, linkDensity, scoringWeights, excludedPages, excludedSections, anchorTextPreferences ) { this.businessName = businessName this.websiteUrl = websiteUrl this.priorityLinks = priorityLinks this.keywordMappings = keywordMappings this.pageConfigs = pageConfigs this.linkDensity = linkDensity this.scoringWeights = scoringWeights this.excludedPages = excludedPages this.excludedSections = excludedSections this.anchorTextPreferences = anchorTextPreferences } } /** * Default configuration */ export const defaultConfig = new InternalLinkConfig( "Rocky Mountain Vending", "https://rockymountainvending.com", [ new PriorityLink( "services/repairs", [ "repair", "repairs", "fix", "broken", "maintenance", "service", "fixing", ], 3, [ "vending machine repairs", "repair services", "vending machine repair", "fixing", ], 10 ), new PriorityLink( "vending-machines/machines-for-sale", [ "buy", "purchase", "sale", "for sale", "machine", "equipment", "acquire", ], 2, [ "vending machines for sale", "buy vending machines", "vending machine sales", "purchase", ], 9 ), new PriorityLink( "vending-machines/machines-we-use", ["machine", "equipment", "vending", "use", "utilize", "employ"], 2, [ "vending machines we use", "our machines", "vending equipment", "vending systems", ], 8 ), new PriorityLink( "about-us", ["about", "company", "us", "we", "our", "team", "business"], 1, ["about us", "about Rocky Mountain Vending", "our company"], 7 ), new PriorityLink( "contact-us", [ "contact", "reach", "call", "phone", "email", "get in touch", "reach out", ], 2, ["contact us", "get in touch", "reach out", "call us"], 8 ), new PriorityLink( "food-and-beverage/healthy-options", ["healthy", "wellness", "nutrition", "snack", "beverage", "options"], 2, [ "healthy vending", "healthy options", "wellness vending", "healthy snacks", ], 7 ), new PriorityLink( "food-and-beverage/traditional-options", ["traditional", "classic", "standard", "conventional", "regular"], 2, ["traditional vending", "classic vending", "standard options"], 6 ), new PriorityLink( "services/parts", ["parts", "component", "replacement", "spare", "accessories", "supplies"], 2, [ "vending machine parts", "parts and support", "replacement parts", "spare parts", ], 7 ), ], [ new KeywordMapping("warehouse", "warehouses", ["warehouses"], true), new KeywordMapping( "auto repair", "auto-repair", ["auto-repair", "automotive repair"], true ), new KeywordMapping("gym", "gyms", ["gyms", "fitness center"], true), new KeywordMapping( "community center", "community-centers", ["community centers", "community facility"], true ), new KeywordMapping( "dance studio", "dance-studios", ["dance studios", "dance facility"], true ), new KeywordMapping( "car wash", "car-washes", ["car washes", "car washing"], true ), new KeywordMapping( "suppliers", "food-and-beverage/suppliers", ["supplier", "wholesale", "distributor"], true ), new KeywordMapping( "faq", "about/faqs", ["faqs", "frequently asked", "questions"], false ), new KeywordMapping( "manuals", "manuals", ["manual", "guide", "documentation"], true ), ], { "": new PageConfig(null, 8, 100), "about-us": new PageConfig(null, 3, 150), "contact-us": new PageConfig(null, 2, 200), services: new PageConfig(null, 6, 80), "vending-machines": new PageConfig(null, 5, 90), manuals: new PageConfig(null, 4, 120), }, new LinkDensityConfig(10, 50, 2, 3), new ScoringWeights(0.2, 0.15, 0.25, 0.15, 0.15, 0.05, 0.05), ["api", "robots", "sitemap"], ["header", "footer", "navigation", "sidebar", "menu"], { preferPageTitle: true, useNaturalLanguage: true, avoidGenericText: [ "click here", "read more", "link", "learn more", "visit", ], } ) /** * Get configuration for a specific page */ export function getPageConfig(route, config = defaultConfig) { // Check exact match first if (config.pageConfigs[route]) { return config.pageConfigs[route] } // Check partial matches (e.g., services/*) for (const [key, pageConfig] of Object.entries(config.pageConfigs)) { if (key !== "" && route.startsWith(key)) { return pageConfig } } // Return default config return new PageConfig( null, config.linkDensity.maxLinksPerPage, config.linkDensity.minWordsBetweenLinks ) } /** * Get all keywords that should be linked to a target page */ export function getKeywordsForTarget(targetRoute, config = defaultConfig) { const keywords = [] // Add priority link keywords const priorityLink = config.priorityLinks.find( (pl) => pl.targetRoute === targetRoute ) if (priorityLink) { keywords.push(...priorityLink.keywords) } // Add keyword mapping keywords for (const mapping of config.keywordMappings) { if (mapping.targetRoute === targetRoute) { keywords.push(mapping.keyword) if (mapping.variations) { keywords.push(...mapping.variations) } } } // Remove duplicates return [...new Set(keywords)] } /** * Get anchor text options for a target page */ export function getAnchorTexts(targetRoute, config = defaultConfig) { // Get from priority links const priorityLink = config.priorityLinks.find( (pl) => pl.targetRoute === targetRoute ) if (priorityLink && priorityLink.anchorTexts.length > 0) { return priorityLink.anchorTexts } // Default to page title return [targetRoute.split("/").pop() || targetRoute] } /** * Check if a route should be excluded from linking */ export function isExcludedRoute(route, config = defaultConfig) { return config.excludedPages.some( (excluded) => route === excluded || route.startsWith(excluded + "/") ) } /** * Calculate maximum links allowed for a page */ export function getMaxLinksForPage(route, config = defaultConfig) { const pageConfig = getPageConfig(route, config) return pageConfig.maxLinksPerPage || config.linkDensity.maxLinksPerPage } /** * Calculate minimum words between links */ export function getMinWordsBetweenLinks(route, config = defaultConfig) { const pageConfig = getPageConfig(route, config) return ( pageConfig.minWordsBetweenLinks || config.linkDensity.minWordsBetweenLinks ) } /** * SEO scoring function */ export function calculatePageScore( route, content, internalLinks, config = defaultConfig ) { const weights = config.scoringWeights let score = 0 // Title score (20%) const hasTitle = content.match(/title:/i) || content.match(/Head>/i) score += hasTitle ? weights.title : 0 // Description score (15%) const hasDescription = content.match(/description:/i) || content.match(/Head>/i) score += hasDescription ? weights.description : 0 // Internal links score (25%) const maxLinks = getMaxLinksForPage(route, config) const linkScore = Math.min(internalLinks.length / maxLinks, 1) * weights.internalLinks score += linkScore // Content length score (15%) const wordCount = content.split(/\s+/).length const contentScore = Math.min(wordCount / 500, 1) * weights.contentLength score += contentScore // Keyword optimization score (15%) const keywordsFound = findKeywordsInContent(content, config).length const keywordScore = Math.min(keywordsFound / 10, 1) * weights.keywordOptimization score += keywordScore // Structure score (5%) const hasGoodStructure = content.match(//i) && content.match(/

/i) score += hasGoodStructure ? weights.structure : 0 // Image optimization score (5%) const imageCount = (content.match(/]*>/gi) || []).length const imageScore = Math.min(imageCount / 3, 1) * weights.imageOptimization score += imageScore return Math.round(score * 100) / 100 } /** * Find keywords in content for SEO analysis */ export function findKeywordsInContent(content, config = defaultConfig) { const keywords = [] const allKeywords = [ ...config.priorityLinks.flatMap((pl) => pl.keywords), ...config.keywordMappings.map((km) => km.keyword), ...config.keywordMappings.flatMap((km) => km.variations || []), ] const contentLower = content.toLowerCase() for (const keyword of allKeywords) { if (contentLower.includes(keyword.toLowerCase())) { keywords.push(keyword) } } return [...new Set(keywords)] }