/** * Internal Link Configuration for SEO Tool * Defines rules for internal link optimization and analysis */ import { PageInfo } from './seo-utils'; /** * Priority link configuration * These links will be prioritized when optimizing internal links */ export interface PriorityLink { targetRoute: string; keywords: string[]; maxLinks: number; anchorTexts: string[]; priority: number; // 1-10, where 10 is highest priority } /** * Keyword to page mapping * Auto-magically link specific keywords to target pages */ export interface KeywordMapping { keyword: string; targetRoute: string; variations?: string[]; useNaturalAnchorText?: boolean; } /** * Page-specific configuration */ export interface PageConfig { excludeSections?: string[]; maxLinksPerPage?: number; minWordsBetweenLinks?: number; preferredAnchorText?: string; } /** * Link density configuration */ export interface LinkDensityConfig { maxLinksPerPage: number; minWordsBetweenLinks: number; maxLinksPerParagraph: number; maxPercentagePerPage: number; } /** * SEO scoring weights */ export interface ScoringWeights { title: number; description: number; internalLinks: number; contentLength: number; keywordOptimization: number; structure: number; imageOptimization: number; } /** * Master configuration */ export interface InternalLinkConfig { // General settings businessName: string; websiteUrl: string; // Priority links priorityLinks: PriorityLink[]; // Keyword mappings keywordMappings: KeywordMapping[]; // Page-specific configurations pageConfigs: Record; // Link density settings linkDensity: LinkDensityConfig; // SEO scoring weights scoringWeights: ScoringWeights; // Excluded pages excludedPages: string[]; // Excluded sections within pages excludedSections: string[]; // Anchor text preferences anchorTextPreferences: { preferPageTitle: boolean; useNaturalLanguage: boolean; avoidGenericText: string[]; }; } /** * Default configuration */ export const defaultConfig: InternalLinkConfig = { businessName: "Rocky Mountain Vending", websiteUrl: "https://rockymountainvending.com", priorityLinks: [ { targetRoute: "services/repairs", keywords: ["repair", "repairs", "fix", "broken", "maintenance", "service", "fixing"], maxLinks: 3, anchorTexts: ["vending machine repairs", "repair services", "vending machine repair", "fixing"], priority: 10 }, { targetRoute: "vending-machines/machines-for-sale", keywords: ["buy", "purchase", "sale", "for sale", "machine", "equipment", "acquire"], maxLinks: 2, anchorTexts: ["vending machines for sale", "buy vending machines", "vending machine sales", "purchase"], priority: 9 }, { targetRoute: "vending-machines/machines-we-use", keywords: ["machine", "equipment", "vending", "use", "utilize", "employ"], maxLinks: 2, anchorTexts: ["vending machines we use", "our machines", "vending equipment", "vending systems"], priority: 8 }, { targetRoute: "about-us", keywords: ["about", "company", "us", "we", "our", "team", "business"], maxLinks: 1, anchorTexts: ["about us", "about Rocky Mountain Vending", "our company"], priority: 7 }, { targetRoute: "contact-us", keywords: ["contact", "reach", "call", "phone", "email", "get in touch", "reach out"], maxLinks: 2, anchorTexts: ["contact us", "get in touch", "reach out", "call us"], priority: 8 }, { targetRoute: "food-and-beverage/healthy-options", keywords: ["healthy", "wellness", "nutrition", "snack", "beverage", "options"], maxLinks: 2, anchorTexts: ["healthy vending", "healthy options", "wellness vending", "healthy snacks"], priority: 7 }, { targetRoute: "food-and-beverage/traditional-options", keywords: ["traditional", "classic", "standard", "conventional", "regular"], maxLinks: 2, anchorTexts: ["traditional vending", "classic vending", "standard options"], priority: 6 }, { targetRoute: "services/parts", keywords: ["parts", "component", "replacement", "spare", "accessories", "supplies"], maxLinks: 2, anchorTexts: ["vending machine parts", "parts and support", "replacement parts", "spare parts"], priority: 7 } ], keywordMappings: [ { keyword: "warehouse", targetRoute: "warehouses", variations: ["warehouses"], useNaturalAnchorText: true }, { keyword: "auto repair", targetRoute: "auto-repair", variations: ["auto-repair", "automotive repair"], useNaturalAnchorText: true }, { keyword: "gym", targetRoute: "gyms", variations: ["gyms", "fitness center"], useNaturalAnchorText: true }, { keyword: "community center", targetRoute: "community-centers", variations: ["community centers", "community facility"], useNaturalAnchorText: true }, { keyword: "dance studio", targetRoute: "dance-studios", variations: ["dance studios", "dance facility"], useNaturalAnchorText: true }, { keyword: "car wash", targetRoute: "car-washes", variations: ["car washes", "car washing"], useNaturalAnchorText: true }, { keyword: "suppliers", targetRoute: "food-and-beverage/suppliers", variations: ["supplier", "wholesale", "distributor"], useNaturalAnchorText: true }, { keyword: "faq", targetRoute: "about/faqs", variations: ["faqs", "frequently asked", "questions"], useNaturalAnchorText: false }, { keyword: "manuals", targetRoute: "manuals", variations: ["manual", "guide", "documentation"], useNaturalAnchorText: true } ], pageConfigs: { "": { // Home page maxLinksPerPage: 8, minWordsBetweenLinks: 100 }, "about-us": { maxLinksPerPage: 3, minWordsBetweenLinks: 150 }, "contact-us": { maxLinksPerPage: 2, minWordsBetweenLinks: 200 }, "services": { maxLinksPerPage: 6, minWordsBetweenLinks: 80 }, "vending-machines": { maxLinksPerPage: 5, minWordsBetweenLinks: 90 }, "manuals": { maxLinksPerPage: 4, minWordsBetweenLinks: 120 } }, linkDensity: { maxLinksPerPage: 10, minWordsBetweenLinks: 50, maxLinksPerParagraph: 2, maxPercentagePerPage: 3 }, scoringWeights: { title: 0.2, description: 0.15, internalLinks: 0.25, contentLength: 0.15, keywordOptimization: 0.15, structure: 0.05, imageOptimization: 0.05 }, excludedPages: [ "api", "robots", "sitemap" ], excludedSections: [ "header", "footer", "navigation", "sidebar", "menu" ], anchorTextPreferences: { preferPageTitle: true, useNaturalLanguage: true, avoidGenericText: ["click here", "read more", "link", "learn more", "visit"] } }; /** * Get configuration for a specific page */ export function getPageConfig(route: string, config: InternalLinkConfig = defaultConfig): PageConfig { // 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 { maxLinksPerPage: config.linkDensity.maxLinksPerPage, minWordsBetweenLinks: config.linkDensity.minWordsBetweenLinks }; } /** * Get all keywords that should be linked to a target page */ export function getKeywordsForTarget(targetRoute: string, config: InternalLinkConfig = defaultConfig): string[] { const keywords: string[] = []; // 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: string, config: InternalLinkConfig = defaultConfig): string[] { // 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: string, config: InternalLinkConfig = defaultConfig): boolean { return config.excludedPages.some(excluded => route === excluded || route.startsWith(excluded + '/') ); } /** * Calculate maximum links allowed for a page */ export function getMaxLinksForPage(route: string, config: InternalLinkConfig = defaultConfig): number { const pageConfig = getPageConfig(route, config); return pageConfig.maxLinksPerPage || config.linkDensity.maxLinksPerPage; } /** * Calculate minimum words between links */ export function getMinWordsBetweenLinks(route: string, config: InternalLinkConfig = defaultConfig): number { const pageConfig = getPageConfig(route, config); return pageConfig.minWordsBetweenLinks || config.linkDensity.minWordsBetweenLinks; } /** * SEO scoring function */ export function calculatePageScore( route: string, content: string, internalLinks: { to: string; text: string }[], config: InternalLinkConfig = defaultConfig ): number { 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: string, config: InternalLinkConfig = defaultConfig): string[] { const keywords: string[] = []; 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)]; }