Rocky_Mountain_Vending/lib/internal-link-config.ts

544 lines
12 KiB
TypeScript

/**
* 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<string, PageConfig>
// 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(/<h[1-6]>/i) && content.match(/<p>/i)
score += hasGoodStructure ? weights.structure : 0
// Image optimization score (5%)
const imageCount = (content.match(/<img[^>]*>/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)]
}