Rocky_Mountain_Vending/scripts/seo-internal-link-tool.js

1150 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import { execSync } from "child_process"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Get the project root
const PROJECT_ROOT = path.join(__dirname, "..")
const APP_DIR = path.join(PROJECT_ROOT, "app")
// Import utilities
import {
discoverPages,
generateSitemapXml,
analyzeInternalLinks,
} from "../lib/seo-utils.js"
import {
defaultConfig as defaultLinkConfig,
calculatePageScore,
getKeywordsForTarget,
getAnchorTexts,
isExcludedRoute,
getMaxLinksForPage,
getMinWordsBetweenLinks,
findKeywordsInContent,
} from "../lib/internal-link-config.js"
// Import readline only when needed
let readline
/**
* Display colored output in console
*/
function colorize(text, color) {
const colors = {
reset: "\x1b[0m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
}
return `${colors[color]}${text}${colors.reset}`
}
/**
* Display section header
*/
function displayHeader(title) {
console.log(`\n${colorize("=".repeat(50), "cyan")}`)
console.log(colorize(` ${title}`, "cyan"))
console.log(colorize("=".repeat(50), "cyan"))
}
/**
* Display success message
*/
function displaySuccess(message) {
console.log(colorize(`${message}`, "green"))
}
/**
* Display error message
*/
function displayError(message) {
console.log(colorize(`${message}`, "red"))
}
/**
* Display warning message
*/
function displayWarning(message) {
console.log(colorize(`${message}`, "yellow"))
}
/**
* Display info message
*/
function displayInfo(message) {
console.log(colorize(` ${message}`, "blue"))
}
/**
* Show help information
*/
function showHelp() {
displayHeader("SEO Internal Link Tool - Help")
console.log(colorize("Usage:", "white"))
console.log(" node seo-internal-link-tool.js <command> [options]")
console.log("")
console.log(colorize("Commands:", "white"))
console.log(" sitemap Generate sitemap from React pages")
console.log(" analyze Analyze internal links")
console.log(" optimize Optimize internal links")
console.log(" report Generate comprehensive SEO report")
console.log(" json-ld Generate JSON-LD structured data")
console.log(" interactive Start interactive mode")
console.log("")
console.log(colorize("Options:", "white"))
console.log(" --output <file> Output file path")
console.log(" --format <format> Output format (json, html, csv, markdown)")
console.log(" --verbose Enable verbose output")
console.log(" --help, -h Show help information")
console.log("")
console.log(colorize("Examples:", "white"))
console.log(" node seo-internal-link-tool.js sitemap")
console.log(" node seo-internal-link-tool.js analyze --output report.json")
console.log(" node seo-internal-link-tool.js interactive")
}
/**
* Read file content with error handling
*/
function readFileContent(filePath) {
try {
return fs.readFileSync(filePath, "utf8")
} catch (error) {
return ""
}
}
/**
* Extract content from page file
*/
function extractPageContent(page) {
return readFileContent(page.filePath)
}
/**
* Generate JSON-LD structured data for a page
*/
function generatePageJsonLD(page, content, linkConfig = defaultLinkConfig) {
const businessConfig = {
name: linkConfig.businessName,
websiteUrl: linkConfig.websiteUrl,
description:
extractPageDescription(content) ||
`${linkConfig.businessName} - ${page.title}`,
}
const jsonLD = {
"@context": "https://schema.org",
"@type": getSchemaTypeForRoute(page.route),
name: page.title,
description: businessConfig.description,
url: `${businessConfig.websiteUrl}${page.url}`,
datePublished: page.lastModified.toISOString(),
dateModified: page.lastModified.toISOString(),
publisher: {
"@type": "Organization",
name: businessConfig.name,
url: businessConfig.websiteUrl,
},
}
// Add route-specific structured data
if (page.route === "") {
jsonLD["@type"] = "WebSite"
jsonLD.about = {
"@type": "LocalBusiness",
name: businessConfig.name,
description: businessConfig.description,
url: businessConfig.websiteUrl,
}
} else if (page.route.includes("service") || page.route.includes("repair")) {
jsonLD["@type"] = "Service"
jsonLD.serviceType = page.title
jsonLD.provider = {
"@type": "LocalBusiness",
name: businessConfig.name,
url: businessConfig.websiteUrl,
}
} else if (page.route.includes("contact-us")) {
jsonLD["@type"] = "ContactPage"
} else if (page.route.includes("about")) {
jsonLD["@type"] = "AboutPage"
}
return jsonLD
}
/**
* Extract page description from content
*/
function extractPageDescription(content) {
const descMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/)
return descMatch ? descMatch[1] : null
}
/**
* Get schema type based on route
*/
function getSchemaTypeForRoute(route) {
if (route === "") return "WebSite"
if (route.includes("service") || route.includes("repair")) return "Service"
if (route.includes("contact-us")) return "ContactPage"
if (route.includes("about")) return "AboutPage"
if (route.includes("product") || route.includes("vending")) return "Product"
return "WebPage"
}
/**
* Generate sitemap from React pages
*/
async function runSitemapGeneration(options) {
displayHeader("Generating Sitemap from React Pages")
try {
// Discover pages
displayInfo("Discovering React pages...")
const pages = discoverPages(APP_DIR)
displaySuccess(`Found ${pages.length} pages`)
// Generate sitemap XML
displayInfo("Generating sitemap XML...")
const sitemapXml = generateSitemapXml(pages)
// Output results
if (options.output) {
fs.writeFileSync(options.output, sitemapXml)
displaySuccess(`Sitemap saved to: ${options.output}`)
} else {
console.log(colorize("\nGenerated Sitemap:", "white"))
console.log(sitemapXml)
}
// Also save to conventional location
const sitemapPath = path.join(PROJECT_ROOT, "out", "sitemap.xml")
fs.writeFileSync(sitemapPath, sitemapXml)
displaySuccess(`Default sitemap location: ${sitemapPath}`)
} catch (error) {
displayError(`Failed to generate sitemap: ${error.message}`)
throw error
}
}
/**
* Analyze internal links
*/
async function runLinkAnalysis(options) {
displayHeader("Analyzing Internal Links")
try {
// Discover pages
displayInfo("Discovering React pages...")
const pages = discoverPages(APP_DIR)
displaySuccess(`Found ${pages.length} pages`)
// Extract content from each page
displayInfo("Extracting page content...")
const contentByRoute = {}
for (const page of pages) {
contentByRoute[page.route] = extractPageContent(page)
}
// Analyze internal links
displayInfo("Analyzing internal links...")
const analysis = analyzeInternalLinks(pages, contentByRoute)
// Display results
displayLinkAnalysisResults(analysis, options)
// Save results if output file specified
if (options.output) {
saveAnalysisResults(analysis, options.output, options.format)
displaySuccess(`Analysis saved to: ${options.output}`)
}
} catch (error) {
displayError(`Failed to analyze links: ${error.message}`)
throw error
}
}
/**
* Display link analysis results
*/
function displayLinkAnalysisResults(analysis, options) {
console.log(colorize(`\n📊 Link Analysis Summary`, "white"))
console.log(colorize(`Total Pages: ${analysis.totalPages}`, "cyan"))
console.log(colorize(`Internal Links: ${analysis.internalLinks}`, "cyan"))
console.log(
colorize(
`Average Link Density: ${analysis.averageLinkDensity.toFixed(2)} links per page`,
"cyan"
)
)
if (analysis.orphanedPages.length > 0) {
displayWarning(`Orphaned Pages: ${analysis.orphanedPages.length}`)
analysis.orphanedPages.forEach((page) => {
console.log(colorize(` - ${page}`, "yellow"))
})
} else {
displaySuccess("No orphaned pages found")
}
if (analysis.brokenLinks.length > 0) {
displayError(`Broken Links: ${analysis.brokenLinks.length}`)
analysis.brokenLinks.forEach((link) => {
console.log(colorize(` - ${link}`, "red"))
})
} else {
displaySuccess("No broken links found")
}
// Page types breakdown
console.log(colorize(`\n📋 Page Types:`, "white"))
for (const [type, count] of Object.entries(analysis.pageTypes)) {
console.log(colorize(` ${type}: ${count} pages`, "blue"))
}
// Pages with issues
if (analysis.pagesWithIssues.length > 0) {
displayWarning(`Pages with Issues: ${analysis.pagesWithIssues.length}`)
analysis.pagesWithIssues.forEach((page) => {
console.log(colorize(` - ${page.url}:`, "yellow"))
page.issues.forEach((issue) => {
console.log(colorize(` * ${issue}`, "yellow"))
})
})
}
}
/**
* Save analysis results
*/
function saveAnalysisResults(results, outputPath, format) {
const output = {
timestamp: new Date().toISOString(),
summary: {
totalPages: results.totalPages,
internalLinks: results.internalLinks,
orphanedPages: results.orphanedPages.length,
brokenLinks: results.brokenLinks.length,
averageLinkDensity: results.averageLinkDensity,
},
details: results,
}
let content
switch (format) {
case "json":
content = JSON.stringify(output, null, 2)
break
case "csv":
content = convertToCSV(results)
break
case "markdown":
content = convertToMarkdown(results)
break
case "html":
content = convertToHTML(results)
break
default:
content = JSON.stringify(output, null, 2)
}
fs.writeFileSync(outputPath, content)
}
/**
* Convert analysis results to CSV
*/
function convertToCSV(results) {
let csv = "Page,Route,Links,Issues\n"
// Add pages with issues
results.pagesWithIssues.forEach((page) => {
csv += `"${page.url}","${page.route}","0","${page.issues.join("; ")}"\n`
})
// Add pages without issues
const allPages = Array.from(
new Set([
...results.orphanedPages,
...results.brokenLinks.map((link) => link.split(" -> ")[0]),
])
)
allPages.forEach((page) => {
csv += `"${page}","${page}","0","No issues"\n`
})
return csv
}
/**
* Convert analysis results to Markdown
*/
function convertToMarkdown(results) {
let md = "# SEO Link Analysis Report\n\n"
md += `*Generated: ${new Date().toISOString()}*\n\n`
md += "## Summary\n\n"
md += `- Total Pages: ${results.totalPages}\n`
md += `- Internal Links: ${results.internalLinks}\n`
md += `- Orphaned Pages: ${results.orphanedPages.length}\n`
md += `- Broken Links: ${results.brokenLinks.length}\n`
md += `- Average Link Density: ${results.averageLinkDensity.toFixed(2)}\n\n`
md += "## Page Types\n\n"
for (const [type, count] of Object.entries(results.pageTypes)) {
md += `- ${type}: ${count} pages\n`
}
md += "\n"
if (results.orphanedPages.length > 0) {
md += "## Orphaned Pages\n\n"
results.orphanedPages.forEach((page) => {
md += `- ${page}\n`
})
md += "\n"
}
if (results.brokenLinks.length > 0) {
md += "## Broken Links\n\n"
results.brokenLinks.forEach((link) => {
md += `- ${link}\n`
})
md += "\n"
}
if (results.pagesWithIssues.length > 0) {
md += "## Pages with Issues\n\n"
results.pagesWithIssues.forEach((page) => {
md += `### ${page.url}\n`
page.issues.forEach((issue) => {
md += `- ${issue}\n`
})
md += "\n"
})
}
return md
}
/**
* Convert analysis results to HTML
*/
function convertToHTML(results) {
return `
<!DOCTYPE html>
<html>
<head>
<title>SEO Link Analysis Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2 { color: #333; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
.issue { color: #d32f2f; }
.warning { color: #f57c00; }
.success { color: #388e3c; }
</style>
</head>
<body>
<h1>SEO Link Analysis Report</h1>
<p>Generated: ${new Date().toISOString()}</p>
<div class="summary">
<h2>Summary</h2>
<p><strong>Total Pages:</strong> ${results.totalPages}</p>
<p><strong>Internal Links:</strong> ${results.internalLinks}</p>
<p><strong>Orphaned Pages:</strong> ${results.orphanedPages.length}</p>
<p><strong>Broken Links:</strong> ${results.brokenLinks.length}</p>
<p><strong>Average Link Density:</strong> ${results.averageLinkDensity.toFixed(2)}</p>
</div>
${
results.orphanedPages.length > 0
? `
<div>
<h2 class="warning">Orphaned Pages</h2>
<ul>
${results.orphanedPages.map((page) => `<li class="warning">${page}</li>`).join("")}
</ul>
</div>`
: ""
}
${
results.brokenLinks.length > 0
? `
<div>
<h2 class="issue">Broken Links</h2>
<ul>
${results.brokenLinks.map((link) => `<li class="issue">${link}</li>`).join("")}
</ul>
</div>`
: ""
}
</body>
</html>`
}
/**
* Optimize internal links
*/
async function runLinkOptimization(options) {
displayHeader("Optimizing Internal Links")
try {
// Load configuration
displayInfo("Loading configuration...")
const linkConfig = defaultLinkConfig
// Discover pages
displayInfo("Discovering React pages...")
const pages = discoverPages(APP_DIR)
displaySuccess(`Found ${pages.length} pages`)
// Extract content from each page
displayInfo("Extracting page content...")
const contentByRoute = {}
for (const page of pages) {
contentByRoute[page.route] = extractPageContent(page)
}
// Optimize links page by page
let totalLinksAdded = 0
const optimizationResults = []
for (const page of pages) {
if (isExcludedRoute(page.route, linkConfig)) {
continue
}
const maxLinks = getMaxLinksForPage(page.route, linkConfig)
const minWordsBetween = getMinWordsBetweenLinks(page.route, linkConfig)
const content = contentByRoute[page.route]
// Calculate SEO score
const internalLinks = extractInternalLinksFromContent(content)
const seoScore = calculatePageScore(
page.route,
content,
internalLinks,
linkConfig
)
// Find keywords to link
const keywordsToLink = findKeywordsInContent(content, linkConfig)
const linksAdded = addOptimalLinks(
content,
keywordsToLink,
pages,
linkConfig
)
if (linksAdded > 0) {
totalLinksAdded += linksAdded
optimizationResults.push({
page: page.route,
linksAdded,
seoScore: seoScore,
keywordsFound: keywordsToLink.length,
})
displaySuccess(`${page.route}: Added ${linksAdded} link(s)`)
}
}
displaySuccess(`Total links added: ${totalLinksAdded}`)
// Save results
if (options.output) {
const results = {
timestamp: new Date().toISOString(),
totalLinksAdded,
pagesOptimized: optimizationResults.length,
details: optimizationResults,
}
fs.writeFileSync(options.output, JSON.stringify(results, null, 2))
displaySuccess(`Optimization results saved to: ${options.output}`)
}
} catch (error) {
displayError(`Failed to optimize links: ${error.message}`)
throw error
}
}
/**
* Extract internal links from content
*/
function extractInternalLinksFromContent(content) {
const links = []
const linkRegex = /<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g
let match
while ((match = linkRegex.exec(content)) !== null) {
if (isInternalLink(match[1])) {
links.push({
to: match[1],
text: match[2].trim(),
})
}
}
return links
}
/**
* Check if link is internal
*/
function isInternalLink(href) {
return (
!href.startsWith("http") &&
!href.startsWith("mailto:") &&
!href.startsWith("tel:")
)
}
/**
* Add optimal links to content
*/
function addOptimalLinks(content, keywords, pages, config) {
let linksAdded = 0
const linkPositions = []
for (const keyword of keywords) {
if (linksAdded >= config.linkDensity.maxLinksPerPage) {
break
}
// Find keyword in content
const keywordRegex = new RegExp(
`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
"gi"
)
let match
while ((match = keywordRegex.exec(content)) !== null) {
if (linksAdded >= config.linkDensity.maxLinksPerPage) {
break
}
// Check distance from other links
const tooClose = linkPositions.some(
(pos) =>
Math.abs(pos - match.index) <
config.linkDensity.minWordsBetweenLinks * 5
)
if (tooClose) continue
// Add link
const targetPage = pages.find(
(p) =>
p.title.toLowerCase().includes(keyword.toLowerCase()) ||
p.route.toLowerCase().includes(keyword.toLowerCase())
)
if (targetPage && !isExcludedRoute(targetPage.route, config)) {
const anchorTexts = getAnchorTexts(targetPage.route, config)
const anchorText = anchorTexts[0] || keyword
// Insert link (simplified - in real implementation, would need to properly handle HTML)
content = insertLink(
content,
match.index,
match.index + match[0].length,
targetPage.url,
anchorText
)
linkPositions.push(match.index)
linksAdded++
break // Only link first occurrence per keyword
}
}
}
return linksAdded
}
/**
* Insert link into content
*/
function insertLink(content, startIndex, endIndex, url, anchorText) {
const before = content.substring(0, startIndex)
const after = content.substring(endIndex)
const link = `<a href="${url}">${anchorText}</a>`
return before + link + after
}
/**
* Generate SEO report
*/
async function runSEOReport(options) {
displayHeader("Generating SEO Report")
try {
// Discover pages
displayInfo("Discovering React pages...")
const pages = discoverPages(APP_DIR)
displaySuccess(`Found ${pages.length} pages`)
// Extract content from each page
displayInfo("Extracting page content...")
const contentByRoute = {}
const pageScores = []
for (const page of pages) {
contentByRoute[page.route] = extractPageContent(page)
// Calculate SEO score
const internalLinks = extractInternalLinksFromContent(
contentByRoute[page.route]
)
const seoScore = calculatePageScore(
page.route,
contentByRoute[page.route],
internalLinks
)
pageScores.push({
page: page.route,
url: page.url,
title: page.title,
priority: page.priority,
score: seoScore,
internalLinks: internalLinks.length,
lastModified: page.lastModified.toISOString(),
})
}
// Sort pages by score
pageScores.sort((a, b) => b.score - a.score)
// Display report
displaySEOResults(pageScores, options)
// Save results
if (options.output) {
const report = {
timestamp: new Date().toISOString(),
totalPages: pages.length,
averageScore:
pageScores.reduce((sum, p) => sum + p.score, 0) / pageScores.length,
pages: pageScores,
}
let content
switch (options.format) {
case "json":
content = JSON.stringify(report, null, 2)
break
case "markdown":
content = generateMarkdownReport(report)
break
case "html":
content = generateHTMLReport(report)
break
default:
content = JSON.stringify(report, null, 2)
}
fs.writeFileSync(options.output, content)
displaySuccess(`Report saved to: ${options.output}`)
}
} catch (error) {
displayError(`Failed to generate report: ${error.message}`)
throw error
}
}
/**
* Display SEO results
*/
function displaySEOResults(pageScores, options) {
console.log(colorize(`\n📊 SEO Analysis Summary`, "white"))
console.log(colorize(`Total Pages Analyzed: ${pageScores.length}`, "cyan"))
const averageScore =
pageScores.reduce((sum, p) => sum + p.score, 0) / pageScores.length
console.log(
colorize(`Average SEO Score: ${averageScore.toFixed(1)}/100`, "cyan")
)
// Top performing pages
console.log(colorize(`\n🏆 Top Performing Pages:`, "white"))
const topPages = pageScores.slice(0, 5)
topPages.forEach((page, index) => {
const scoreColor =
page.score >= 80 ? "green" : page.score >= 60 ? "yellow" : "red"
console.log(
colorize(
`${index + 1}. ${page.title} (${page.route}) - Score: ${page.score.toFixed(1)}`,
scoreColor
)
)
})
// Pages needing improvement
const poorPages = pageScores.filter((p) => p.score < 60)
if (poorPages.length > 0) {
console.log(colorize(`\n⚠ Pages Needing Improvement:`, "yellow"))
poorPages.slice(0, 3).forEach((page) => {
console.log(
colorize(
`- ${page.title} (${page.route}) - Score: ${page.score.toFixed(1)}`,
"red"
)
)
})
}
}
/**
* Generate markdown report
*/
function generateMarkdownReport(report) {
let md = "# SEO Report\n\n"
md += `*Generated: ${report.timestamp}*\n\n`
md += "## Summary\n\n"
md += `- Total Pages: ${report.totalPages}\n`
md += `- Average Score: ${report.averageScore.toFixed(1)}/100\n\n`
md += "## Page Scores\n\n"
report.pages.forEach((page, index) => {
const status =
page.score >= 80
? "Excellent"
: page.score >= 60
? "Good"
: "Needs Improvement"
const statusColor =
page.score >= 80 ? "green" : page.score >= 60 ? "yellow" : "red"
md += `${index + 1}. **${page.title}** (${page.route})\n`
md += ` - Score: ${page.score.toFixed(1)}/100 (${status})\n`
md += ` - Internal Links: ${page.internalLinks}\n`
md += ` - Priority: ${page.priority}\n`
md += ` - Last Modified: ${new Date(page.lastModified).toLocaleDateString()}\n\n`
})
return md
}
/**
* Generate HTML report
*/
function generateHTMLReport(report) {
return `
<!DOCTYPE html>
<html>
<head>
<title>SEO Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2 { color: #333; }
.page { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.score { font-size: 24px; font-weight: bold; }
.excellent { color: #388e3c; }
.good { color: #f57c00; }
.poor { color: #d32f2f; }
</style>
</head>
<body>
<h1>SEO Report</h1>
<p>Generated: ${report.timestamp}</p>
<h2>Summary</h2>
<p><strong>Total Pages:</strong> ${report.totalPages}</p>
<p><strong>Average Score:</strong> ${report.averageScore.toFixed(1)}/100</p>
<h2>Page Scores</h2>
${report.pages
.map((page, index) => {
const status =
page.score >= 80 ? "excellent" : page.score >= 60 ? "good" : "poor"
return `
<div class="page">
<h3>${index + 1}. ${page.title} (${page.route})</h3>
<p class="score ${status}">${page.score.toFixed(1)}/100</p>
<p>Internal Links: ${page.internalLinks}</p>
<p>Priority: ${page.priority}</p>
<p>Last Modified: ${new Date(page.lastModified).toLocaleDateString()}</p>
</div>`
})
.join("")}
</body>
</html>`
}
/**
* Generate JSON-LD structured data
*/
async function runJsonLDGeneration(options) {
displayHeader("Generating JSON-LD Structured Data")
try {
// Discover pages
displayInfo("Discovering React pages...")
const pages = discoverPages(APP_DIR)
displaySuccess(`Found ${pages.length} pages`)
// Generate JSON-LD for each page
const jsonLDData = {}
for (const page of pages) {
const content = extractPageContent(page)
const jsonLD = generatePageJsonLD(page, content)
jsonLDData[page.route] = jsonLD
if (options.verbose) {
displaySuccess(`Generated JSON-LD for: ${page.route}`)
}
}
// Output results
if (options.output) {
fs.writeFileSync(options.output, JSON.stringify(jsonLDData, null, 2))
displaySuccess(`JSON-LD data saved to: ${options.output}`)
} else {
console.log(colorize("\nGenerated JSON-LD Structured Data:", "white"))
console.log(JSON.stringify(jsonLDData, null, 2))
}
// Also save individual files (skip dynamic routes to avoid filesystem issues)
const outputDir = path.join(PROJECT_ROOT, "public", "json-ld")
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
for (const [route, jsonLD] of Object.entries(jsonLDData)) {
// Skip dynamic routes
if (route.includes(":") || route.includes("...")) {
continue
}
let filename
if (route === "") {
filename = "home.jsonld"
} else {
filename = `${route.replace(/\//g, "_")}.jsonld`
}
const filepath = path.join(outputDir, filename)
fs.writeFileSync(filepath, JSON.stringify(jsonLD, null, 2))
}
displaySuccess(`Individual JSON-LD files saved to: ${outputDir}`)
} catch (error) {
displayError(`Failed to generate JSON-LD: ${error.message}`)
throw error
}
}
/**
* Start interactive mode
*/
async function startInteractiveMode(options) {
displayHeader("SEO Internal Link Tool - Interactive Mode")
displayInfo('Type "help" for available commands or "exit" to quit')
// Import readline only when needed
try {
readline = await import("readline")
} catch (error) {
displayError("Interactive mode requires Node.js readline module")
process.exit(1)
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const question = (query) =>
new Promise((resolve) => rl.question(query, resolve))
while (true) {
const command = await question("\nseo-tool> ")
if (command.toLowerCase() === "exit" || command.toLowerCase() === "quit") {
break
}
if (command.toLowerCase() === "help") {
showHelp()
continue
}
const args = command.split(" ")
const cmd = args[0]
const cmdOptions = {
output: null,
format: "console",
verbose: false,
}
// Parse options
for (let i = 1; i < args.length; i++) {
const arg = args[i]
if (arg === "--output" && i + 1 < args.length) {
cmdOptions.output = args[i + 1]
i++
} else if (arg === "--format" && i + 1 < args.length) {
cmdOptions.format = args[i + 1]
i++
} else if (arg === "--verbose" || arg === "-v") {
cmdOptions.verbose = true
}
}
try {
switch (cmd) {
case "sitemap":
await runSitemapGeneration(cmdOptions)
break
case "analyze":
await runLinkAnalysis(cmdOptions)
break
case "optimize":
await runLinkOptimization(cmdOptions)
break
case "report":
await runSEOReport(cmdOptions)
break
case "json-ld":
await runJsonLDGeneration(cmdOptions)
break
default:
displayError(`Unknown command: ${cmd}`)
showHelp()
}
} catch (error) {
displayError(`Error: ${error.message}`)
}
}
rl.close()
displaySuccess("Goodbye!")
}
/**
* Main CLI dispatcher
*/
function main() {
const args = process.argv.slice(2)
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
showHelp()
return
}
const command = args[0]
const options = {
output: null,
format: "console",
verbose: false,
}
// Parse options
for (let i = 1; i < args.length; i++) {
const arg = args[i]
if (arg === "--output" && i + 1 < args.length) {
options.output = args[i + 1]
i++
} else if (arg === "--format" && i + 1 < args.length) {
options.format = args[i + 1]
i++
} else if (arg === "--verbose" || arg === "-v") {
options.verbose = true
}
}
// Check if we need to import readline for interactive mode
if (command === "interactive") {
try {
readline = require("readline")
} catch (error) {
displayError("Interactive mode requires Node.js readline module")
process.exit(1)
}
}
switch (command) {
case "sitemap":
executeCommand(runSitemapGeneration, options)
break
case "analyze":
executeCommand(runLinkAnalysis, options)
break
case "optimize":
executeCommand(runLinkOptimization, options)
break
case "report":
executeCommand(runSEOReport, options)
break
case "json-ld":
executeCommand(runJsonLDGeneration, options)
break
case "interactive":
startInteractiveMode(options)
break
default:
displayError(`Unknown command: ${command}`)
console.log("")
showHelp()
process.exit(1)
}
}
/**
* Execute function and handle errors
*/
async function executeCommand(commandFn, options) {
try {
await commandFn(options)
} catch (error) {
displayError(`Error: ${error.message}`)
if (options.verbose) {
console.error(error)
}
process.exit(1)
}
}
// Export functions for external use
export {
main,
executeCommand,
displayHeader,
displaySuccess,
displayError,
displayWarning,
displayInfo,
runSitemapGeneration,
runLinkAnalysis,
runLinkOptimization,
runSEOReport,
runJsonLDGeneration,
}
// Run if called directly
if (
import.meta.url === `file://${process.argv[1]}` ||
process.argv[1]?.endsWith("seo-internal-link-tool.js")
) {
main()
}