- Updated all component headers and documentation
- Changed navbar and footer branding
- Updated homepage hero badge
- Modified page title in index.html
- Simplified footer text to 'Built with ❤️'
- Consistent V2 capitalization across all references
330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.SEOAnalyzerService = void 0;
|
|
const axios_1 = __importDefault(require("axios"));
|
|
const url_1 = require("url");
|
|
const logger_1 = require("../lib/logger");
|
|
class SEOAnalyzerService {
|
|
async analyzeSEO(url) {
|
|
logger_1.logger.info(`Starting SEO analysis for: ${url}`);
|
|
const result = {
|
|
url,
|
|
flags: {
|
|
robotsTxtStatus: 'not_found',
|
|
robotsTxtRules: { status: 'not_found', rules: [], sitemaps: [] },
|
|
sitemapPresent: false,
|
|
noindex: false,
|
|
nofollow: false,
|
|
hasTitle: false,
|
|
hasDescription: false,
|
|
openGraphPresent: false,
|
|
twitterCardPresent: false,
|
|
},
|
|
metaTags: {
|
|
openGraph: {},
|
|
twitter: {},
|
|
},
|
|
recommendations: [],
|
|
warnings: [],
|
|
score: 0,
|
|
};
|
|
try {
|
|
const robotsAnalysis = await this.analyzeRobotsTxt(url);
|
|
result.flags.robotsTxtStatus = robotsAnalysis.status;
|
|
result.flags.robotsTxtRules = robotsAnalysis;
|
|
result.flags.sitemapPresent = robotsAnalysis.sitemaps.length > 0;
|
|
const pageAnalysis = await this.analyzePageContent(url);
|
|
result.metaTags = pageAnalysis.metaTags;
|
|
result.flags = { ...result.flags, ...pageAnalysis.flags };
|
|
this.generateSEORecommendations(result);
|
|
logger_1.logger.info(`SEO analysis completed for: ${url}`, {
|
|
score: result.score,
|
|
robotsStatus: result.flags.robotsTxtStatus,
|
|
hasTitle: result.flags.hasTitle,
|
|
noindex: result.flags.noindex
|
|
});
|
|
}
|
|
catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown SEO analysis error';
|
|
result.warnings.push(`SEO analysis failed: ${errorMessage}`);
|
|
logger_1.logger.error(`SEO analysis failed for ${url}:`, error);
|
|
}
|
|
return result;
|
|
}
|
|
async analyzeRobotsTxt(url) {
|
|
try {
|
|
const parsedUrl = new url_1.URL(url);
|
|
const robotsUrl = `${parsedUrl.protocol}//${parsedUrl.host}/robots.txt`;
|
|
const response = await axios_1.default.get(robotsUrl, {
|
|
timeout: 5000,
|
|
headers: {
|
|
'User-Agent': 'RedirectIntelligence-Bot/2.0',
|
|
},
|
|
});
|
|
const robotsContent = response.data;
|
|
return this.parseRobotsTxt(robotsContent);
|
|
}
|
|
catch (error) {
|
|
if (axios_1.default.isAxiosError(error) && error.response?.status === 404) {
|
|
return { status: 'not_found', rules: [], sitemaps: [] };
|
|
}
|
|
logger_1.logger.warn(`Failed to fetch robots.txt for ${url}:`, error);
|
|
return { status: 'error', rules: [], sitemaps: [] };
|
|
}
|
|
}
|
|
parseRobotsTxt(content) {
|
|
const lines = content.split('\n').map(line => line.trim());
|
|
const rules = [];
|
|
const sitemaps = [];
|
|
let currentRule = null;
|
|
for (const line of lines) {
|
|
if (line.startsWith('#') || line === '')
|
|
continue;
|
|
const [directive, ...valueParts] = line.split(':');
|
|
const value = valueParts.join(':').trim();
|
|
switch (directive.toLowerCase()) {
|
|
case 'user-agent':
|
|
if (currentRule) {
|
|
rules.push(currentRule);
|
|
}
|
|
currentRule = {
|
|
userAgent: value,
|
|
allow: [],
|
|
disallow: [],
|
|
};
|
|
break;
|
|
case 'allow':
|
|
if (currentRule) {
|
|
currentRule.allow.push(value);
|
|
}
|
|
break;
|
|
case 'disallow':
|
|
if (currentRule) {
|
|
currentRule.disallow.push(value);
|
|
}
|
|
break;
|
|
case 'crawl-delay':
|
|
if (currentRule) {
|
|
currentRule.crawlDelay = parseInt(value) || undefined;
|
|
}
|
|
break;
|
|
case 'sitemap':
|
|
sitemaps.push(value);
|
|
break;
|
|
}
|
|
}
|
|
if (currentRule) {
|
|
rules.push(currentRule);
|
|
}
|
|
return {
|
|
status: 'found',
|
|
rules,
|
|
sitemaps,
|
|
};
|
|
}
|
|
async analyzePageContent(url) {
|
|
try {
|
|
const response = await axios_1.default.get(url, {
|
|
timeout: 10000,
|
|
headers: {
|
|
'User-Agent': 'RedirectIntelligence-Bot/2.0 (SEO Analysis)',
|
|
},
|
|
maxContentLength: 1024 * 1024,
|
|
});
|
|
const html = response.data;
|
|
return this.parseHTMLContent(html);
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn(`Failed to fetch page content for ${url}:`, error);
|
|
return {
|
|
metaTags: { openGraph: {}, twitter: {} },
|
|
flags: {
|
|
hasTitle: false,
|
|
hasDescription: false,
|
|
noindex: false,
|
|
nofollow: false,
|
|
openGraphPresent: false,
|
|
twitterCardPresent: false,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
parseHTMLContent(html) {
|
|
const metaTags = {
|
|
openGraph: {},
|
|
twitter: {},
|
|
};
|
|
const flags = {
|
|
hasTitle: false,
|
|
hasDescription: false,
|
|
noindex: false,
|
|
nofollow: false,
|
|
openGraphPresent: false,
|
|
twitterCardPresent: false,
|
|
};
|
|
const titleMatch = html.match(/<title[^>]*>(.*?)<\/title>/is);
|
|
if (titleMatch) {
|
|
metaTags.title = this.cleanText(titleMatch[1]);
|
|
flags.hasTitle = true;
|
|
flags.titleLength = metaTags.title.length;
|
|
}
|
|
const metaTagRegex = /<meta\s+([^>]*?)>/gi;
|
|
let metaMatch;
|
|
while ((metaMatch = metaTagRegex.exec(html)) !== null) {
|
|
const attributes = this.parseAttributes(metaMatch[1]);
|
|
if (attributes.name === 'description') {
|
|
metaTags.description = attributes.content;
|
|
flags.hasDescription = true;
|
|
flags.descriptionLength = attributes.content?.length || 0;
|
|
}
|
|
else if (attributes.name === 'robots') {
|
|
metaTags.robots = attributes.content;
|
|
flags.metaRobots = attributes.content;
|
|
if (attributes.content?.toLowerCase().includes('noindex')) {
|
|
flags.noindex = true;
|
|
}
|
|
if (attributes.content?.toLowerCase().includes('nofollow')) {
|
|
flags.nofollow = true;
|
|
}
|
|
}
|
|
else if (attributes.name === 'viewport') {
|
|
metaTags.viewport = attributes.content;
|
|
}
|
|
else if (attributes.property?.startsWith('og:')) {
|
|
const ogProperty = attributes.property.substring(3);
|
|
metaTags.openGraph[ogProperty] = attributes.content;
|
|
flags.openGraphPresent = true;
|
|
}
|
|
else if (attributes.name?.startsWith('twitter:')) {
|
|
const twitterProperty = attributes.name.substring(8);
|
|
metaTags.twitter[twitterProperty] = attributes.content;
|
|
flags.twitterCardPresent = true;
|
|
}
|
|
}
|
|
const canonicalMatch = html.match(/<link\s+([^>]*?)rel=['"]canonical['"][^>]*>/i);
|
|
if (canonicalMatch) {
|
|
const attributes = this.parseAttributes(canonicalMatch[1]);
|
|
metaTags.canonical = attributes.href;
|
|
flags.canonicalUrl = attributes.href;
|
|
}
|
|
return { metaTags, flags };
|
|
}
|
|
parseAttributes(attributeString) {
|
|
const attributes = {};
|
|
const attrRegex = /(\w+)=['"]([^'"]*)['"]/g;
|
|
let match;
|
|
while ((match = attrRegex.exec(attributeString)) !== null) {
|
|
attributes[match[1].toLowerCase()] = match[2];
|
|
}
|
|
return attributes;
|
|
}
|
|
cleanText(text) {
|
|
return text.replace(/\s+/g, ' ').trim();
|
|
}
|
|
generateSEORecommendations(result) {
|
|
let score = 100;
|
|
if (!result.flags.hasTitle) {
|
|
result.warnings.push('Missing page title');
|
|
result.recommendations.push('Add a descriptive page title');
|
|
score -= 15;
|
|
}
|
|
else if (result.flags.titleLength) {
|
|
if (result.flags.titleLength < 30) {
|
|
result.warnings.push('Title is too short');
|
|
result.recommendations.push('Expand title to 30-60 characters for better SEO');
|
|
score -= 5;
|
|
}
|
|
else if (result.flags.titleLength > 60) {
|
|
result.warnings.push('Title is too long');
|
|
result.recommendations.push('Shorten title to under 60 characters');
|
|
score -= 5;
|
|
}
|
|
}
|
|
if (!result.flags.hasDescription) {
|
|
result.warnings.push('Missing meta description');
|
|
result.recommendations.push('Add a meta description (150-160 characters)');
|
|
score -= 10;
|
|
}
|
|
else if (result.flags.descriptionLength) {
|
|
if (result.flags.descriptionLength < 120) {
|
|
result.warnings.push('Meta description is too short');
|
|
result.recommendations.push('Expand meta description to 150-160 characters');
|
|
score -= 3;
|
|
}
|
|
else if (result.flags.descriptionLength > 160) {
|
|
result.warnings.push('Meta description is too long');
|
|
result.recommendations.push('Shorten meta description to under 160 characters');
|
|
score -= 3;
|
|
}
|
|
}
|
|
if (result.flags.robotsTxtStatus === 'not_found') {
|
|
result.recommendations.push('Consider adding a robots.txt file');
|
|
score -= 5;
|
|
}
|
|
else if (result.flags.robotsTxtStatus === 'error') {
|
|
result.warnings.push('Robots.txt file has errors');
|
|
result.recommendations.push('Fix robots.txt file errors');
|
|
score -= 8;
|
|
}
|
|
if (!result.flags.sitemapPresent) {
|
|
result.recommendations.push('Add XML sitemap references to robots.txt');
|
|
score -= 5;
|
|
}
|
|
if (result.flags.noindex) {
|
|
result.warnings.push('Page is set to noindex');
|
|
result.recommendations.push('Remove noindex if you want this page indexed');
|
|
score -= 20;
|
|
}
|
|
if (!result.flags.openGraphPresent) {
|
|
result.recommendations.push('Add Open Graph meta tags for social media sharing');
|
|
score -= 5;
|
|
}
|
|
if (!result.flags.twitterCardPresent) {
|
|
result.recommendations.push('Add Twitter Card meta tags for Twitter sharing');
|
|
score -= 3;
|
|
}
|
|
if (!result.flags.canonicalUrl) {
|
|
result.recommendations.push('Add canonical URL to prevent duplicate content issues');
|
|
score -= 5;
|
|
}
|
|
result.score = Math.max(0, score);
|
|
if (result.score >= 90) {
|
|
result.recommendations.push('Excellent SEO setup!');
|
|
}
|
|
else if (result.score >= 70) {
|
|
result.recommendations.push('Good SEO with room for minor improvements');
|
|
}
|
|
else if (result.score >= 50) {
|
|
result.recommendations.push('SEO needs improvement for better search visibility');
|
|
}
|
|
else {
|
|
result.recommendations.push('SEO requires immediate attention');
|
|
}
|
|
}
|
|
async quickSEOCheck(url) {
|
|
try {
|
|
const analysis = await this.analyzeSEO(url);
|
|
const robotsBlocked = analysis.flags.robotsTxtRules.rules.some(rule => rule.userAgent === '*' && rule.disallow.includes('/'));
|
|
return {
|
|
noindex: analysis.flags.noindex,
|
|
nofollow: analysis.flags.nofollow,
|
|
robotsBlocked,
|
|
hasTitle: analysis.flags.hasTitle,
|
|
};
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn(`Quick SEO check failed for ${url}:`, error);
|
|
return {
|
|
noindex: false,
|
|
nofollow: false,
|
|
robotsBlocked: false,
|
|
hasTitle: false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
exports.SEOAnalyzerService = SEOAnalyzerService;
|
|
//# sourceMappingURL=seo-analyzer.service.js.map
|