Rocky_Mountain_Vending/lib/internal-link-config.js
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

283 lines
10 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)];
}