Files
url_tracker_tool/apps/api/dist/services/seo-analyzer.service.js
Andrei 58f8093689 Rebrand from 'Redirect Intelligence v2' to 'URL Tracker Tool V2' throughout UI
- 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
2025-08-19 19:12:23 +00:00

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