Rocky_Mountain_Vending/lib/internal-link-config.js

468 lines
11 KiB
JavaScript

/**
* 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(/<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, 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)]
}