468 lines
11 KiB
JavaScript
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)]
|
|
}
|