Rocky_Mountain_Vending/lib/internal-link-config.ts
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

444 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)];
}