@lynxflow/seo-engine 1.8.30 โ 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -117
- package/dist/ai-copilot-client.d.ts +2 -0
- package/dist/built-in-locations.d.ts +22 -0
- package/dist/content-ai-arsenal.d.ts +28 -0
- package/dist/existing-media-harvester.d.ts +33 -0
- package/dist/extended-schemas.d.ts +84 -0
- package/dist/features-knowledge-harvester.d.ts +56 -0
- package/dist/geo-citation-scorer.d.ts +34 -0
- package/dist/i18n-detector.d.ts +1 -1
- package/dist/index.d.ts +32 -7
- package/dist/index.js +567 -83
- package/dist/index.mjs +2062 -169
- package/dist/internal-pagerank-graph.d.ts +59 -0
- package/dist/knowledge-bank-builder.d.ts +91 -0
- package/dist/marketing-skills-engine.d.ts +74 -0
- package/dist/matrix-engine.d.ts +29 -0
- package/dist/power-words-psychology.d.ts +27 -15
- package/dist/public-manifest-engine.d.ts +58 -0
- package/dist/rag-knowledge-engine.d.ts +50 -0
- package/dist/rate-limited-translator.d.ts +19 -0
- package/dist/token-quota-manager.d.ts +4 -0
- package/package.json +11 -3
- package/TUTORIEL_INTEGRATION_SITE.md +0 -283
- package/connectors/cloudflare-worker/worker.js +0 -106
- package/connectors/laravel/LynxSeoController.php +0 -37
- package/connectors/wordpress/lynxseo-admin-app.js +0 -2611
- package/connectors/wordpress/lynxseo-connector.php +0 -1712
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/lynxflow-seo-engine-1.2.0.tgz +0 -0
- package/src/ad-intelligence-cro.ts +0 -140
- package/src/ai-bots-log-analyzer.ts +0 -127
- package/src/ai-copilot-client.ts +0 -99
- package/src/analytics-client.ts +0 -315
- package/src/auth-key.ts +0 -262
- package/src/backlinks-client.ts +0 -85
- package/src/brand-dna-calendar.ts +0 -120
- package/src/brand-icons.ts +0 -138
- package/src/built-in-locations.ts +0 -170
- package/src/content-ai-arsenal.ts +0 -268
- package/src/copy-frameworks-master.ts +0 -89
- package/src/cro-copywriting-engine.ts +0 -105
- package/src/crosslink-scorer.ts +0 -101
- package/src/embeddable-seo-widget.ts +0 -57
- package/src/engine.test.ts +0 -386
- package/src/engine.ts +0 -813
- package/src/existing-media-harvester.ts +0 -98
- package/src/extended-schemas.ts +0 -589
- package/src/features-knowledge-harvester.ts +0 -190
- package/src/free-public-tools.ts +0 -224
- package/src/geo-mesh-linking.ts +0 -93
- package/src/google-business-profile.ts +0 -280
- package/src/i18n-detector.ts +0 -98
- package/src/i18n-dictionary.ts +0 -422
- package/src/index.ts +0 -164
- package/src/indexnow-client.ts +0 -94
- package/src/instant-matrix-search.ts +0 -48
- package/src/isr-cache-manager.ts +0 -78
- package/src/keyword-permutator.ts +0 -118
- package/src/knowledge-bank-builder.ts +0 -299
- package/src/knowledge-graph-linker.ts +0 -81
- package/src/lago-token-meter.ts +0 -120
- package/src/legal-disclaimers.ts +0 -407
- package/src/llm-content-cleaner.ts +0 -64
- package/src/llm-prompt.ts +0 -323
- package/src/master-marketing-engine.ts +0 -314
- package/src/matrix-engine.ts +0 -1204
- package/src/mcp-seo-server.ts +0 -108
- package/src/ngram-density-analyzer.ts +0 -108
- package/src/og-image-generator.ts +0 -109
- package/src/power-words-psychology.ts +0 -369
- package/src/public-manifest-engine.ts +0 -109
- package/src/rag-knowledge-engine.ts +0 -163
- package/src/rank-math-parity.ts +0 -235
- package/src/rate-limited-translator.ts +0 -110
- package/src/real-reviews-sync.ts +0 -207
- package/src/rss-syndication-feed.ts +0 -56
- package/src/schema-builder.ts +0 -134
- package/src/seo-opportunities-decay.ts +0 -154
- package/src/serp-client.ts +0 -89
- package/src/serp-history-alerts.ts +0 -75
- package/src/site-auditor.ts +0 -83
- package/src/site-crawler.ts +0 -406
- package/src/slug-engine.ts +0 -183
- package/src/social-ads-seo.ts +0 -211
- package/src/social-growth-suite.ts +0 -310
- package/src/social-trend-seo.ts +0 -103
- package/src/social-video-seo.ts +0 -100
- package/src/team-rbac.ts +0 -61
- package/src/technical-rules-auditor.ts +0 -171
- package/src/token-quota-manager.ts +0 -91
- package/src/types.ts +0 -143
- package/src/ui-icons.ts +0 -134
- package/src/urlytics-engine.ts +0 -97
- package/src/video-youtube-analyzer.ts +0 -81
- package/src/yoast-parity.ts +0 -161
- package/tsconfig.json +0 -15
package/src/site-crawler.ts
DELETED
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ๐ท๏ธ Deep Site Crawler & Technical SEO Auditor (Inspired by CrawlSEO & Seonaut)
|
|
3
|
-
*
|
|
4
|
-
* High-performance, zero-dependency recursive site crawler & technical health inspector.
|
|
5
|
-
* - Crawls live websites or crawls pre-rendered HTML snapshots
|
|
6
|
-
* - Analyzes status codes, redirects, canonicals, titles, H1s, meta descriptions, image ALTs
|
|
7
|
-
* - Detects 30+ SEO issues classified by severity (critical, warning, info)
|
|
8
|
-
* - Computes a comprehensive 0-100 SEO Health Score with category breakdowns
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface CrawlIssue {
|
|
12
|
-
url: string;
|
|
13
|
-
type: string;
|
|
14
|
-
severity: "critical" | "warning" | "info";
|
|
15
|
-
message: string;
|
|
16
|
-
recommendation: string;
|
|
17
|
-
details?: Record<string, unknown>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CrawledPageData {
|
|
21
|
-
url: string;
|
|
22
|
-
statusCode: number;
|
|
23
|
-
responseTimeMs: number;
|
|
24
|
-
title: string | null;
|
|
25
|
-
description: string | null;
|
|
26
|
-
h1: string | null;
|
|
27
|
-
h1Count: number;
|
|
28
|
-
canonical: string | null;
|
|
29
|
-
isCanonicalMatch: boolean;
|
|
30
|
-
wordCount: number;
|
|
31
|
-
imagesWithoutAlt: number;
|
|
32
|
-
internalLinksCount: number;
|
|
33
|
-
externalLinksCount: number;
|
|
34
|
-
issues: CrawlIssue[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface SiteAuditSummary {
|
|
38
|
-
domain: string;
|
|
39
|
-
crawledPagesCount: number;
|
|
40
|
-
healthScore: number; // 0 to 100
|
|
41
|
-
categories: {
|
|
42
|
-
metaAndTags: number; // 0-100
|
|
43
|
-
contentQuality: number; // 0-100
|
|
44
|
-
indexingAndLinks: number; // 0-100
|
|
45
|
-
performanceAndStatus: number; // 0-100
|
|
46
|
-
};
|
|
47
|
-
totalIssues: {
|
|
48
|
-
critical: number;
|
|
49
|
-
warning: number;
|
|
50
|
-
info: number;
|
|
51
|
-
};
|
|
52
|
-
brokenLinks404: string[];
|
|
53
|
-
redirectChains: { source: string; target: string; statusCode: number }[];
|
|
54
|
-
pages: CrawledPageData[];
|
|
55
|
-
executionTimeMs: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface CrawlOptions {
|
|
59
|
-
maxPages?: number;
|
|
60
|
-
maxDepth?: number;
|
|
61
|
-
timeoutMs?: number;
|
|
62
|
-
concurrency?: number;
|
|
63
|
-
userAgent?: string;
|
|
64
|
-
includeExternalLinksCheck?: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export class DeepCrawlerAuditor {
|
|
68
|
-
private static DEFAULT_USER_AGENT = "LynxFlowSeoBot/1.2 (+https://lynxintel.io/bot; technical audit)";
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Performs an instant in-memory technical audit on raw HTML and metadata.
|
|
72
|
-
*/
|
|
73
|
-
static inspectHtmlSnapshot(url: string, html: string, statusCode = 200, responseTimeMs = 45): CrawledPageData {
|
|
74
|
-
const issues: CrawlIssue[] = [];
|
|
75
|
-
|
|
76
|
-
// 1. Status code checks
|
|
77
|
-
if (statusCode >= 400 && statusCode < 500) {
|
|
78
|
-
issues.push({
|
|
79
|
-
url,
|
|
80
|
-
type: "broken_page_404",
|
|
81
|
-
severity: "critical",
|
|
82
|
-
message: `HTTP Client Error: Page returned ${statusCode}`,
|
|
83
|
-
recommendation: "Fix broken link or configure a 301 permanent redirect to a relevant page.",
|
|
84
|
-
});
|
|
85
|
-
} else if (statusCode >= 500) {
|
|
86
|
-
issues.push({
|
|
87
|
-
url,
|
|
88
|
-
type: "server_error_500",
|
|
89
|
-
severity: "critical",
|
|
90
|
-
message: `HTTP Server Error: Page returned ${statusCode}`,
|
|
91
|
-
recommendation: "Inspect server logs and resolve backend application crash.",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 2. Title extraction & checks
|
|
96
|
-
const titleMatch = html.match(/<title[^>]*>([^<]*)<\/title>/i);
|
|
97
|
-
const title = titleMatch ? titleMatch[1].trim() : null;
|
|
98
|
-
|
|
99
|
-
if (!title) {
|
|
100
|
-
issues.push({
|
|
101
|
-
url,
|
|
102
|
-
type: "missing_title",
|
|
103
|
-
severity: "critical",
|
|
104
|
-
message: "Missing <title> tag.",
|
|
105
|
-
recommendation: "Add an explicit, compelling <title> between 30 and 60 characters.",
|
|
106
|
-
});
|
|
107
|
-
} else if (title.length < 20) {
|
|
108
|
-
issues.push({
|
|
109
|
-
url,
|
|
110
|
-
type: "short_title",
|
|
111
|
-
severity: "warning",
|
|
112
|
-
message: `Title is too short (${title.length} chars): "${title}"`,
|
|
113
|
-
recommendation: "Expand title to at least 30 characters including primary keyword and brand name.",
|
|
114
|
-
});
|
|
115
|
-
} else if (title.length > 70) {
|
|
116
|
-
issues.push({
|
|
117
|
-
url,
|
|
118
|
-
type: "long_title",
|
|
119
|
-
severity: "warning",
|
|
120
|
-
message: `Title is too long (${title.length} chars), risk of SERP truncation.`,
|
|
121
|
-
recommendation: "Keep title under 60-65 characters for optimal desktop & mobile display.",
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 3. Meta description checks
|
|
126
|
-
const descMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i) ||
|
|
127
|
-
html.match(/<meta[^>]*content=["']([^"']*)["'][^>]*name=["']description["'][^>]*>/i);
|
|
128
|
-
const description = descMatch ? descMatch[1].trim() : null;
|
|
129
|
-
|
|
130
|
-
if (!description) {
|
|
131
|
-
issues.push({
|
|
132
|
-
url,
|
|
133
|
-
type: "missing_meta_description",
|
|
134
|
-
severity: "critical",
|
|
135
|
-
message: "Missing meta description.",
|
|
136
|
-
recommendation: "Add a compelling meta description between 120 and 160 characters with clear call-to-action.",
|
|
137
|
-
});
|
|
138
|
-
} else if (description.length < 70) {
|
|
139
|
-
issues.push({
|
|
140
|
-
url,
|
|
141
|
-
type: "short_meta_description",
|
|
142
|
-
severity: "warning",
|
|
143
|
-
message: `Meta description is too short (${description.length} chars).`,
|
|
144
|
-
recommendation: "Expand meta description to at least 120 characters.",
|
|
145
|
-
});
|
|
146
|
-
} else if (description.length > 180) {
|
|
147
|
-
issues.push({
|
|
148
|
-
url,
|
|
149
|
-
type: "long_meta_description",
|
|
150
|
-
severity: "info",
|
|
151
|
-
message: `Meta description exceeds 180 chars (${description.length} chars).`,
|
|
152
|
-
recommendation: "Shorten meta description to 155-160 characters.",
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 4. H1 checks
|
|
157
|
-
const h1Matches = Array.from(html.matchAll(/<h1[^>]*>([^<]*)<\/h1>/gi)).map((m) => m[1].trim());
|
|
158
|
-
const h1Count = h1Matches.length;
|
|
159
|
-
const h1 = h1Count > 0 ? h1Matches[0] : null;
|
|
160
|
-
|
|
161
|
-
if (h1Count === 0) {
|
|
162
|
-
issues.push({
|
|
163
|
-
url,
|
|
164
|
-
type: "missing_h1",
|
|
165
|
-
severity: "critical",
|
|
166
|
-
message: "Missing <h1> headline.",
|
|
167
|
-
recommendation: "Add exactly one descriptive <h1> headline containing your target keyword.",
|
|
168
|
-
});
|
|
169
|
-
} else if (h1Count > 1) {
|
|
170
|
-
issues.push({
|
|
171
|
-
url,
|
|
172
|
-
type: "multiple_h1",
|
|
173
|
-
severity: "warning",
|
|
174
|
-
message: `Found ${h1Count} <h1> tags on the page.`,
|
|
175
|
-
recommendation: "Use only one single <h1> per page and structure other sections with <h2>/<h3>.",
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 5. Canonical checks
|
|
180
|
-
const canonicalMatch = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']*)["'][^>]*>/i);
|
|
181
|
-
const canonical = canonicalMatch ? canonicalMatch[1].trim() : null;
|
|
182
|
-
const isCanonicalMatch = canonical ? canonical.replace(/\/$/, "") === url.replace(/\/$/, "") : false;
|
|
183
|
-
|
|
184
|
-
if (!canonical) {
|
|
185
|
-
issues.push({
|
|
186
|
-
url,
|
|
187
|
-
type: "missing_canonical",
|
|
188
|
-
severity: "warning",
|
|
189
|
-
message: "Missing self-referencing canonical tag.",
|
|
190
|
-
recommendation: "Add a <link rel='canonical' href='...' /> tag to prevent duplicate content indexation.",
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 6. Image ALT checks
|
|
195
|
-
const imgMatches = Array.from(html.matchAll(/<img([^>]*)>/gi));
|
|
196
|
-
let imagesWithoutAlt = 0;
|
|
197
|
-
for (const match of imgMatches) {
|
|
198
|
-
const imgTag = match[1];
|
|
199
|
-
if (!/alt=["'][^"']+["']/i.test(imgTag)) {
|
|
200
|
-
imagesWithoutAlt++;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (imagesWithoutAlt > 0) {
|
|
205
|
-
issues.push({
|
|
206
|
-
url,
|
|
207
|
-
type: "images_missing_alt",
|
|
208
|
-
severity: "warning",
|
|
209
|
-
message: `${imagesWithoutAlt} image(s) missing descriptive 'alt' attribute.`,
|
|
210
|
-
recommendation: "Add descriptive ALT text for SEO image search and accessibility compliance.",
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 7. Word count & Content Depth
|
|
215
|
-
const cleanText = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
216
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
217
|
-
.replace(/<[^>]+>/g, " ")
|
|
218
|
-
.replace(/\s+/g, " ")
|
|
219
|
-
.trim();
|
|
220
|
-
const wordCount = cleanText.split(" ").filter((w) => w.length > 1).length;
|
|
221
|
-
|
|
222
|
-
if (wordCount < 150 && statusCode === 200) {
|
|
223
|
-
issues.push({
|
|
224
|
-
url,
|
|
225
|
-
type: "thin_content",
|
|
226
|
-
severity: "warning",
|
|
227
|
-
message: `Thin content detected (${wordCount} words).`,
|
|
228
|
-
recommendation: "Expand content to at least 300-500 words to provide authoritative value.",
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 8. Link counters
|
|
233
|
-
const linkMatches = Array.from(html.matchAll(/<a[^>]*href=["']([^"']*)["'][^>]*>/gi));
|
|
234
|
-
let internalLinksCount = 0;
|
|
235
|
-
let externalLinksCount = 0;
|
|
236
|
-
|
|
237
|
-
for (const match of linkMatches) {
|
|
238
|
-
const href = match[1];
|
|
239
|
-
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
240
|
-
try {
|
|
241
|
-
const targetHost = new URL(href).hostname;
|
|
242
|
-
const currentHost = new URL(url).hostname;
|
|
243
|
-
if (targetHost === currentHost) internalLinksCount++;
|
|
244
|
-
else externalLinksCount++;
|
|
245
|
-
} catch {
|
|
246
|
-
externalLinksCount++;
|
|
247
|
-
}
|
|
248
|
-
} else if (href.startsWith("/") || href.startsWith("#") || href.startsWith(".")) {
|
|
249
|
-
internalLinksCount++;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
url,
|
|
255
|
-
statusCode,
|
|
256
|
-
responseTimeMs,
|
|
257
|
-
title,
|
|
258
|
-
description,
|
|
259
|
-
h1,
|
|
260
|
-
h1Count,
|
|
261
|
-
canonical,
|
|
262
|
-
isCanonicalMatch,
|
|
263
|
-
wordCount,
|
|
264
|
-
imagesWithoutAlt,
|
|
265
|
-
internalLinksCount,
|
|
266
|
-
externalLinksCount,
|
|
267
|
-
issues,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Crawls a full website domain recursively and produces an institutional SEO audit summary.
|
|
273
|
-
*/
|
|
274
|
-
static async crawlAndAuditDomain(targetUrl: string, options: CrawlOptions = {}): Promise<SiteAuditSummary> {
|
|
275
|
-
const t0 = performance.now();
|
|
276
|
-
const maxPages = options.maxPages || 30;
|
|
277
|
-
const timeoutMs = options.timeoutMs || 8000;
|
|
278
|
-
const userAgent = options.userAgent || this.DEFAULT_USER_AGENT;
|
|
279
|
-
|
|
280
|
-
const baseDomain = targetUrl.replace(/\/$/, "");
|
|
281
|
-
const baseHost = new URL(baseDomain).hostname;
|
|
282
|
-
|
|
283
|
-
const visited = new Set<string>();
|
|
284
|
-
const queue: string[] = [baseDomain];
|
|
285
|
-
const crawledPages: CrawledPageData[] = [];
|
|
286
|
-
const brokenLinks404: string[] = [];
|
|
287
|
-
const redirectChains: { source: string; target: string; statusCode: number }[] = [];
|
|
288
|
-
|
|
289
|
-
while (queue.length > 0 && crawledPages.length < maxPages) {
|
|
290
|
-
const currentUrl = queue.shift()!;
|
|
291
|
-
const normalized = currentUrl.replace(/\/$/, "");
|
|
292
|
-
|
|
293
|
-
if (visited.has(normalized)) continue;
|
|
294
|
-
visited.add(normalized);
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const fetchStart = performance.now();
|
|
298
|
-
const controller = new AbortController();
|
|
299
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
300
|
-
|
|
301
|
-
const res = await fetch(currentUrl, {
|
|
302
|
-
signal: controller.signal,
|
|
303
|
-
headers: { "User-Agent": userAgent },
|
|
304
|
-
});
|
|
305
|
-
clearTimeout(timeoutId);
|
|
306
|
-
|
|
307
|
-
const fetchTimeMs = Math.round(performance.now() - fetchStart);
|
|
308
|
-
const statusCode = res.status;
|
|
309
|
-
|
|
310
|
-
if (statusCode === 404) {
|
|
311
|
-
brokenLinks404.push(currentUrl);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (res.redirected && res.url !== currentUrl) {
|
|
315
|
-
redirectChains.push({ source: currentUrl, target: res.url, statusCode });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const html = await res.text();
|
|
319
|
-
const pageAudit = this.inspectHtmlSnapshot(currentUrl, html, statusCode, fetchTimeMs);
|
|
320
|
-
crawledPages.push(pageAudit);
|
|
321
|
-
|
|
322
|
-
// Discover new internal links
|
|
323
|
-
const hrefMatches = Array.from(html.matchAll(/<a[^>]*href=["']([^"'#]+)["']/gi));
|
|
324
|
-
for (const match of hrefMatches) {
|
|
325
|
-
const rawHref = match[1].trim();
|
|
326
|
-
try {
|
|
327
|
-
const resolved = new URL(rawHref, currentUrl).href.replace(/\/$/, "");
|
|
328
|
-
const parsed = new URL(resolved);
|
|
329
|
-
|
|
330
|
-
if (parsed.hostname === baseHost && !visited.has(resolved) && !queue.includes(resolved)) {
|
|
331
|
-
// Ignore media and asset extensions
|
|
332
|
-
if (!/\.(png|jpg|jpeg|gif|svg|webp|css|js|pdf|zip)$/i.test(parsed.pathname)) {
|
|
333
|
-
queue.push(resolved);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
} catch {
|
|
337
|
-
// ignore invalid URL
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
} catch (err: any) {
|
|
341
|
-
crawledPages.push({
|
|
342
|
-
url: currentUrl,
|
|
343
|
-
statusCode: 0,
|
|
344
|
-
responseTimeMs: 0,
|
|
345
|
-
title: null,
|
|
346
|
-
description: null,
|
|
347
|
-
h1: null,
|
|
348
|
-
h1Count: 0,
|
|
349
|
-
canonical: null,
|
|
350
|
-
isCanonicalMatch: false,
|
|
351
|
-
wordCount: 0,
|
|
352
|
-
imagesWithoutAlt: 0,
|
|
353
|
-
internalLinksCount: 0,
|
|
354
|
-
externalLinksCount: 0,
|
|
355
|
-
issues: [{
|
|
356
|
-
url: currentUrl,
|
|
357
|
-
type: "fetch_timeout_error",
|
|
358
|
-
severity: "critical",
|
|
359
|
-
message: `Connection Error: ${err?.message || "Failed to reach server"}`,
|
|
360
|
-
recommendation: "Ensure server is reachable and responds in under 5 seconds.",
|
|
361
|
-
}],
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Compute Health Score & Category Ratings
|
|
367
|
-
let criticalCount = 0;
|
|
368
|
-
let warningCount = 0;
|
|
369
|
-
let infoCount = 0;
|
|
370
|
-
|
|
371
|
-
for (const p of crawledPages) {
|
|
372
|
-
for (const iss of p.issues) {
|
|
373
|
-
if (iss.severity === "critical") criticalCount++;
|
|
374
|
-
else if (iss.severity === "warning") warningCount++;
|
|
375
|
-
else infoCount++;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const totalPages = Math.max(1, crawledPages.length);
|
|
380
|
-
const penalty = (criticalCount * 12 + warningCount * 4 + infoCount * 1) / totalPages;
|
|
381
|
-
const healthScore = Math.max(10, Math.min(100, Math.round(100 - penalty)));
|
|
382
|
-
|
|
383
|
-
const categories = {
|
|
384
|
-
metaAndTags: Math.max(20, Math.min(100, Math.round(100 - (criticalCount * 8 + warningCount * 3) / totalPages))),
|
|
385
|
-
contentQuality: Math.max(30, Math.min(100, Math.round(100 - (warningCount * 5) / totalPages))),
|
|
386
|
-
indexingAndLinks: Math.max(25, Math.min(100, Math.round(100 - (brokenLinks404.length * 15) / totalPages))),
|
|
387
|
-
performanceAndStatus: Math.max(40, Math.min(100, Math.round(100 - (criticalCount * 10) / totalPages))),
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
domain: baseDomain,
|
|
392
|
-
crawledPagesCount: crawledPages.length,
|
|
393
|
-
healthScore,
|
|
394
|
-
categories,
|
|
395
|
-
totalIssues: {
|
|
396
|
-
critical: criticalCount,
|
|
397
|
-
warning: warningCount,
|
|
398
|
-
info: infoCount,
|
|
399
|
-
},
|
|
400
|
-
brokenLinks404,
|
|
401
|
-
redirectChains,
|
|
402
|
-
pages: crawledPages,
|
|
403
|
-
executionTimeMs: Math.round(performance.now() - t0),
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
}
|
package/src/slug-engine.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global Multilingual Programmatic SEO Slug Engine
|
|
3
|
-
* Enforces the 6 golden rules of SEO-optimized URL paths across 10+ languages:
|
|
4
|
-
* English, French, German, Spanish, Italian, Portuguese, Dutch, Russian, Swedish, Polish.
|
|
5
|
-
*
|
|
6
|
-
* 1. Short & focused (3-5 target keywords)
|
|
7
|
-
* 2. Hyphen (-) as the sole delimiter
|
|
8
|
-
* 3. 100% lowercase, diacritics/accents stripped
|
|
9
|
-
* 4. Stop words removed for 10+ international languages
|
|
10
|
-
* 5. Zero dates/numbers for evergreen content
|
|
11
|
-
* 6. Primary keyword positioned first
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export const MULTILINGUAL_STOP_WORDS: Record<string, Set<string>> = {
|
|
15
|
-
// ๐ฌ๐ง English
|
|
16
|
-
en: new Set([
|
|
17
|
-
"the", "a", "an", "and", "or", "for", "with", "without", "in", "on", "at",
|
|
18
|
-
"by", "to", "of", "from", "as", "is", "are", "be", "this", "that", "these",
|
|
19
|
-
"those", "how", "what", "which", "who", "whom", "when", "where", "why",
|
|
20
|
-
"your", "my", "our", "their", "its", "into", "over", "after", "before",
|
|
21
|
-
]),
|
|
22
|
-
|
|
23
|
-
// ๐ซ๐ท French
|
|
24
|
-
fr: new Set([
|
|
25
|
-
"le", "la", "les", "un", "une", "des", "du", "de", "d", "l", "pour", "avec",
|
|
26
|
-
"sans", "sur", "sous", "dans", "par", "et", "ou", "a", "au", "aux", "en",
|
|
27
|
-
"ce", "cet", "cette", "ces", "son", "sa", "ses", "leur", "leurs", "notre",
|
|
28
|
-
"votre", "nos", "vos", "qui", "que", "quoi", "dont", "comment", "pourquoi",
|
|
29
|
-
]),
|
|
30
|
-
|
|
31
|
-
// ๐ฉ๐ช German
|
|
32
|
-
de: new Set([
|
|
33
|
-
"der", "die", "das", "ein", "eine", "eines", "einer", "einem", "einen",
|
|
34
|
-
"und", "oder", "fur", "fรผr", "mit", "ohne", "auf", "unter", "in", "im", "von",
|
|
35
|
-
"vom", "zu", "zum", "zur", "bei", "beim", "nach", "aus", "uber", "รผber", "vor",
|
|
36
|
-
"wie", "was", "wer", "warum", "ihr", "ihre", "sein", "seine", "mein",
|
|
37
|
-
]),
|
|
38
|
-
|
|
39
|
-
// ๐ช๐ธ Spanish
|
|
40
|
-
es: new Set([
|
|
41
|
-
"el", "la", "los", "las", "un", "una", "unos", "unas", "de", "del", "para",
|
|
42
|
-
"con", "sin", "en", "por", "y", "o", "a", "al", "este", "esta", "estos",
|
|
43
|
-
"estas", "su", "sus", "mi", "mis", "tu", "tus", "como", "que", "cual",
|
|
44
|
-
]),
|
|
45
|
-
|
|
46
|
-
// ๐ฎ๐น Italian
|
|
47
|
-
it: new Set([
|
|
48
|
-
"il", "lo", "la", "i", "gli", "le", "un", "uno", "una", "un'", "di", "del",
|
|
49
|
-
"dello", "della", "dei", "degli", "delle", "a", "al", "allo", "alla", "ai",
|
|
50
|
-
"agli", "alle", "da", "dal", "in", "con", "su", "per", "tra", "fra", "e", "o",
|
|
51
|
-
"come", "cosa", "perche", "questo", "questa",
|
|
52
|
-
]),
|
|
53
|
-
|
|
54
|
-
// ๐ต๐น Portuguese
|
|
55
|
-
pt: new Set([
|
|
56
|
-
"o", "a", "os", "as", "um", "uma", "uns", "umas", "de", "do", "da", "dos",
|
|
57
|
-
"das", "em", "no", "na", "nos", "nas", "para", "por", "com", "sem", "e",
|
|
58
|
-
"ou", "como", "que", "qual", "seu", "sua", "seus", "suas", "este", "esta",
|
|
59
|
-
]),
|
|
60
|
-
|
|
61
|
-
// ๐ณ๐ฑ Dutch
|
|
62
|
-
nl: new Set([
|
|
63
|
-
"de", "het", "een", "en", "of", "voor", "met", "zonder", "in", "op", "bij",
|
|
64
|
-
"van", "naar", "door", "over", "onder", "als", "hoe", "wat", "wie", "waarom",
|
|
65
|
-
"zijn", "haar", "hun", "onze", "uw",
|
|
66
|
-
]),
|
|
67
|
-
|
|
68
|
-
// ๐ท๐บ Russian (Transliterated / Latinized)
|
|
69
|
-
ru: new Set([
|
|
70
|
-
"i", "v", "na", "s", "po", "dlya", "ot", "iz", "k", "o", "za", "kak",
|
|
71
|
-
"chto", "gde", "kogda", "pochemu", "eto", "etot", "eta", "eti", "ili",
|
|
72
|
-
]),
|
|
73
|
-
|
|
74
|
-
// ๐ธ๐ช Swedish
|
|
75
|
-
sv: new Set([
|
|
76
|
-
"en", "ett", "den", "det", "de", "och", "eller", "for", "med", "utan",
|
|
77
|
-
"pa", "under", "i", "av", "till", "fran", "om", "hur", "vad", "varfor",
|
|
78
|
-
]),
|
|
79
|
-
|
|
80
|
-
// ๐ต๐ฑ Polish
|
|
81
|
-
pl: new Set([
|
|
82
|
-
"i", "w", "we", "z", "ze", "na", "do", "dla", "od", "o", "po", "za", "jak",
|
|
83
|
-
"co", "gdzie", "dlaczego", "to", "ten", "ta", "te", "lub", "albo",
|
|
84
|
-
]),
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Global merged stop words set
|
|
88
|
-
const ALL_STOP_WORDS = new Set<string>();
|
|
89
|
-
for (const langSet of Object.values(MULTILINGUAL_STOP_WORDS)) {
|
|
90
|
-
for (const word of langSet) {
|
|
91
|
-
ALL_STOP_WORDS.add(word);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface SlugOptions {
|
|
96
|
-
language?: string; // e.g. "en", "fr", "de", "es", "all"
|
|
97
|
-
removeStopWords?: boolean;
|
|
98
|
-
stripDates?: boolean;
|
|
99
|
-
maxWords?: number;
|
|
100
|
-
maxLength?: number;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Normalizes any text into a clean, canonical SEO slug with multilingual stop word filtering.
|
|
105
|
-
*/
|
|
106
|
-
export function cleanSeoSlug(input: string, options: SlugOptions = {}): string {
|
|
107
|
-
const {
|
|
108
|
-
language = "en",
|
|
109
|
-
removeStopWords = true,
|
|
110
|
-
stripDates = true,
|
|
111
|
-
maxWords = 5,
|
|
112
|
-
maxLength = 75,
|
|
113
|
-
} = options;
|
|
114
|
-
|
|
115
|
-
if (!input || typeof input !== "string") {
|
|
116
|
-
return "page";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let text = input
|
|
120
|
-
.normalize("NFKD")
|
|
121
|
-
.replace(/[\u0300-\u036f]/g, "")
|
|
122
|
-
.toLowerCase()
|
|
123
|
-
.trim();
|
|
124
|
-
|
|
125
|
-
if (stripDates) {
|
|
126
|
-
// Strip 4-digit years (2010-2039) and transient top lists (e.g. "top 10")
|
|
127
|
-
text = text.replace(/\b(20[1-3][0-9])\b/g, "");
|
|
128
|
-
text = text.replace(/\btop\s*\d+\s*/g, "");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let tokens = text
|
|
132
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
133
|
-
.trim()
|
|
134
|
-
.split(/\s+/)
|
|
135
|
-
.filter(Boolean);
|
|
136
|
-
|
|
137
|
-
if (removeStopWords) {
|
|
138
|
-
const activeSet = language === "all" || !MULTILINGUAL_STOP_WORDS[language]
|
|
139
|
-
? ALL_STOP_WORDS
|
|
140
|
-
: MULTILINGUAL_STOP_WORDS[language];
|
|
141
|
-
|
|
142
|
-
tokens = tokens.filter((token) => !activeSet.has(token) && token.length > 1);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
tokens = tokens.slice(0, maxWords);
|
|
146
|
-
let result = tokens.join("-");
|
|
147
|
-
|
|
148
|
-
if (result.length > maxLength) {
|
|
149
|
-
result = result.substring(0, maxLength).replace(/-[^-]*$/, "");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return result || "page";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface SlugValidationResult {
|
|
156
|
-
isValid: boolean;
|
|
157
|
-
errors: string[];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Validates a slug against universal SEO golden rules.
|
|
162
|
-
*/
|
|
163
|
-
export function validateSeoSlug(slug: string): SlugValidationResult {
|
|
164
|
-
const errors: string[] = [];
|
|
165
|
-
|
|
166
|
-
if (!slug || slug.length < 2) {
|
|
167
|
-
errors.push("Slug must contain at least 2 characters");
|
|
168
|
-
}
|
|
169
|
-
if (slug.length > 75) {
|
|
170
|
-
errors.push("Slug should not exceed 75 characters for optimal CTR");
|
|
171
|
-
}
|
|
172
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
173
|
-
errors.push("Slug must be 100% lowercase alphanumeric with single hyphen (-) separators");
|
|
174
|
-
}
|
|
175
|
-
if (/(?:20[1-3][0-9])/.test(slug)) {
|
|
176
|
-
errors.push("Evergreen slug should not contain yearly date stamps");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
isValid: errors.length === 0,
|
|
181
|
-
errors,
|
|
182
|
-
};
|
|
183
|
-
}
|