@aexol/spectral 0.9.43 → 0.9.44

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.
Files changed (39) hide show
  1. package/dist/extensions/seo/analyzers/content-optimize.d.ts +3 -0
  2. package/dist/extensions/seo/analyzers/content-optimize.d.ts.map +1 -0
  3. package/dist/extensions/seo/analyzers/content-optimize.js +190 -0
  4. package/dist/extensions/seo/analyzers/crawler.d.ts +4 -0
  5. package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -0
  6. package/dist/extensions/seo/analyzers/crawler.js +288 -0
  7. package/dist/extensions/seo/analyzers/keywords.d.ts +9 -0
  8. package/dist/extensions/seo/analyzers/keywords.d.ts.map +1 -0
  9. package/dist/extensions/seo/analyzers/keywords.js +183 -0
  10. package/dist/extensions/seo/analyzers/performance.d.ts +6 -0
  11. package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -0
  12. package/dist/extensions/seo/analyzers/performance.js +257 -0
  13. package/dist/extensions/seo/analyzers/readability.d.ts +4 -0
  14. package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -0
  15. package/dist/extensions/seo/analyzers/readability.js +121 -0
  16. package/dist/extensions/seo/index.d.ts.map +1 -1
  17. package/dist/extensions/seo/index.js +47 -9
  18. package/dist/extensions/seo/tools/batch.d.ts +4 -0
  19. package/dist/extensions/seo/tools/batch.d.ts.map +1 -0
  20. package/dist/extensions/seo/tools/batch.js +193 -0
  21. package/dist/extensions/seo/tools/crawl.d.ts +4 -0
  22. package/dist/extensions/seo/tools/crawl.d.ts.map +1 -0
  23. package/dist/extensions/seo/tools/crawl.js +166 -0
  24. package/dist/extensions/seo/tools/full-audit.d.ts +1 -0
  25. package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
  26. package/dist/extensions/seo/tools/keywords.d.ts +4 -0
  27. package/dist/extensions/seo/tools/keywords.d.ts.map +1 -0
  28. package/dist/extensions/seo/tools/keywords.js +234 -0
  29. package/dist/extensions/seo/tools/performance.d.ts +4 -0
  30. package/dist/extensions/seo/tools/performance.d.ts.map +1 -0
  31. package/dist/extensions/seo/tools/performance.js +201 -0
  32. package/dist/extensions/seo/types.d.ts +156 -0
  33. package/dist/extensions/seo/types.d.ts.map +1 -1
  34. package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
  35. package/dist/extensions/seo/utils/url-safety.js +7 -5
  36. package/dist/server/handlers/paths-pick-directory.d.ts +0 -27
  37. package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
  38. package/dist/server/handlers/paths-pick-directory.js +86 -92
  39. package/package.json +1 -1
@@ -0,0 +1,3 @@
1
+ import type { ContentOptimizationResult, PageData } from "../types.js";
2
+ export declare function optimizeContent(pageData: PageData): ContentOptimizationResult;
3
+ //# sourceMappingURL=content-optimize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-optimize.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/content-optimize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,QAAQ,EAAe,MAAM,aAAa,CAAC;AAepF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,yBAAyB,CAyL7E"}
@@ -0,0 +1,190 @@
1
+ const TITLE_MIN = 30;
2
+ const TITLE_MAX = 60;
3
+ const TITLE_IDEAL_MIN = 50;
4
+ const TITLE_IDEAL_MAX = 60;
5
+ const DESC_MIN = 120;
6
+ const DESC_MAX = 160;
7
+ const DESC_IDEAL_MIN = 150;
8
+ const DESC_IDEAL_MAX = 160;
9
+ const CONTENT_MIN_WORDS = 300;
10
+ const CONTENT_GOOD_WORDS = 1500;
11
+ export function optimizeContent(pageData) {
12
+ const issues = [];
13
+ let titleScore = 100;
14
+ let descriptionScore = 100;
15
+ let headingStructureScore = 100;
16
+ let contentLengthScore = 100;
17
+ // === Title analysis ===
18
+ const title = pageData.title ?? "";
19
+ const titleLen = title.length;
20
+ if (!title) {
21
+ titleScore = 0;
22
+ issues.push({
23
+ category: "Title",
24
+ severity: "critical",
25
+ issue: "Missing page title",
26
+ recommendation: "Add a descriptive <title> tag between 50-60 characters.",
27
+ });
28
+ }
29
+ else if (titleLen < TITLE_MIN) {
30
+ titleScore -= 30;
31
+ issues.push({
32
+ category: "Title",
33
+ severity: "high",
34
+ issue: `Title too short (${titleLen} chars, minimum ${TITLE_MIN})`,
35
+ recommendation: "Expand title to 50-60 characters. Include primary keyword near the beginning.",
36
+ });
37
+ }
38
+ else if (titleLen > TITLE_MAX) {
39
+ titleScore -= 25;
40
+ issues.push({
41
+ category: "Title",
42
+ severity: "medium",
43
+ issue: `Title too long (${titleLen} chars, max ${TITLE_MAX})`,
44
+ recommendation: `Trim title to ${TITLE_MAX} characters. Current title will be truncated in SERP.`,
45
+ });
46
+ }
47
+ else if (titleLen < TITLE_IDEAL_MIN) {
48
+ titleScore -= 10;
49
+ issues.push({
50
+ category: "Title",
51
+ severity: "low",
52
+ issue: `Title slightly short (${titleLen} chars, ideal ${TITLE_IDEAL_MIN}-${TITLE_IDEAL_MAX})`,
53
+ recommendation: "Expand title slightly for better SERP visibility.",
54
+ });
55
+ }
56
+ // === Meta description analysis ===
57
+ const desc = pageData.metaDescription ?? "";
58
+ const descLen = desc.length;
59
+ if (!desc) {
60
+ descriptionScore = 0;
61
+ issues.push({
62
+ category: "Meta Description",
63
+ severity: "critical",
64
+ issue: "Missing meta description",
65
+ recommendation: "Add a meta description between 150-160 characters with a clear call to action.",
66
+ });
67
+ }
68
+ else if (descLen < DESC_MIN) {
69
+ descriptionScore -= 35;
70
+ issues.push({
71
+ category: "Meta Description",
72
+ severity: "high",
73
+ issue: `Meta description too short (${descLen} chars, minimum ${DESC_MIN})`,
74
+ recommendation: "Expand meta description to 150-160 characters. Include primary and secondary keywords naturally.",
75
+ });
76
+ }
77
+ else if (descLen > DESC_MAX) {
78
+ descriptionScore -= 20;
79
+ issues.push({
80
+ category: "Meta Description",
81
+ severity: "medium",
82
+ issue: `Meta description too long (${descLen} chars, max ${DESC_MAX})`,
83
+ recommendation: `Trim description to ${DESC_MAX} characters. Google typically truncates at ~160 chars.`,
84
+ });
85
+ }
86
+ else if (descLen < DESC_IDEAL_MIN) {
87
+ descriptionScore -= 5;
88
+ issues.push({
89
+ category: "Meta Description",
90
+ severity: "low",
91
+ issue: `Meta description slightly short (${descLen} chars, ideal ${DESC_IDEAL_MIN}-${DESC_IDEAL_MAX})`,
92
+ recommendation: "Slightly expand description to fill available SERP space.",
93
+ });
94
+ }
95
+ // Duplicate title/description check
96
+ if (title === desc) {
97
+ titleScore -= 40;
98
+ descriptionScore -= 40;
99
+ issues.push({
100
+ category: "Content",
101
+ severity: "high",
102
+ issue: "Page title and meta description are identical",
103
+ recommendation: "Title and description serve different purposes. Make them distinct — title for keyword targeting, description for CTR.",
104
+ });
105
+ }
106
+ // === Heading structure analysis ===
107
+ if (pageData.h1Count === 0) {
108
+ headingStructureScore -= 30;
109
+ issues.push({
110
+ category: "Heading Structure",
111
+ severity: "high",
112
+ issue: "No H1 heading found",
113
+ recommendation: "Add a single, descriptive H1 that includes your primary keyword.",
114
+ });
115
+ }
116
+ else if (pageData.h1Count > 1) {
117
+ headingStructureScore -= 20;
118
+ issues.push({
119
+ category: "Heading Structure",
120
+ severity: "medium",
121
+ issue: `Multiple H1 headings (${pageData.h1Count})`,
122
+ recommendation: "Use exactly one H1 per page. Convert extra H1s to H2s.",
123
+ });
124
+ }
125
+ if (pageData.h2Count === 0 && pageData.wordCount > 200) {
126
+ headingStructureScore -= 15;
127
+ issues.push({
128
+ category: "Heading Structure",
129
+ severity: "medium",
130
+ issue: "No H2 headings for content longer than 200 words",
131
+ recommendation: "Add H2 headings to break content into scannable sections.",
132
+ });
133
+ }
134
+ // Check heading hierarchy gaps (e.g., H1 → H3 skipping H2)
135
+ const headingLevels = pageData.headings.map((h) => h.level);
136
+ for (let i = 1; i < headingLevels.length; i++) {
137
+ const gap = headingLevels[i] - headingLevels[i - 1];
138
+ if (gap > 1) {
139
+ headingStructureScore -= 10;
140
+ issues.push({
141
+ category: "Heading Structure",
142
+ severity: "low",
143
+ issue: `Heading level skip: H${headingLevels[i - 1]} → H${headingLevels[i]} without intermediate level`,
144
+ recommendation: `Add an H${headingLevels[i - 1] + 1} between these headings for proper hierarchy.`,
145
+ });
146
+ break; // Report once
147
+ }
148
+ }
149
+ // Check suspicious headings
150
+ const suspiciousHeadings = pageData.headings.filter((h) => h.suspicious);
151
+ if (suspiciousHeadings.length > 0) {
152
+ headingStructureScore -= 10;
153
+ issues.push({
154
+ category: "Heading Structure",
155
+ severity: "low",
156
+ issue: `${suspiciousHeadings.length} suspicious heading(s) found (e.g., "click here", "learn more")`,
157
+ recommendation: "Use descriptive, keyword-rich headings instead of generic labels.",
158
+ });
159
+ }
160
+ // === Content length analysis ===
161
+ if (pageData.wordCount < CONTENT_MIN_WORDS) {
162
+ contentLengthScore -= 40;
163
+ issues.push({
164
+ category: "Content Length",
165
+ severity: "high",
166
+ issue: `Thin content: ${pageData.wordCount} words (minimum ${CONTENT_MIN_WORDS})`,
167
+ recommendation: `Expand content to at least ${CONTENT_MIN_WORDS} words. Pages under ${CONTENT_MIN_WORDS} words are considered thin content by Google.`,
168
+ });
169
+ }
170
+ else if (pageData.wordCount < CONTENT_GOOD_WORDS) {
171
+ contentLengthScore -= 15;
172
+ issues.push({
173
+ category: "Content Length",
174
+ severity: "low",
175
+ issue: `Content below optimal length: ${pageData.wordCount} words (optimal ${CONTENT_GOOD_WORDS}+)`,
176
+ recommendation: "Consider expanding to 1500+ words for competitive topics.",
177
+ });
178
+ }
179
+ // === Overall score ===
180
+ const overallScore = Math.round((titleScore * 0.25 + descriptionScore * 0.20 + headingStructureScore * 0.25 + contentLengthScore * 0.30));
181
+ return {
182
+ url: pageData.url,
183
+ titleScore,
184
+ descriptionScore,
185
+ headingStructureScore,
186
+ contentLengthScore,
187
+ overallScore,
188
+ issues,
189
+ };
190
+ }
@@ -0,0 +1,4 @@
1
+ import type { SeoConfig } from "../config.js";
2
+ import type { CrawlConfig, CrawlResult } from "../types.js";
3
+ export declare function crawlSite(startUrl: string, config: SeoConfig, crawlConfig?: Partial<CrawlConfig>): Promise<CrawlResult>;
4
+ //# sourceMappingURL=crawler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crawler.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/crawler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAe,WAAW,EAAE,WAAW,EAA8B,MAAM,aAAa,CAAC;AA4FrG,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GACjC,OAAO,CAAC,WAAW,CAAC,CAkKtB"}
@@ -0,0 +1,288 @@
1
+ import { fetchPage, fetchRobotsTxt } from "../utils/fetcher.js";
2
+ import { parsePage } from "../utils/parser.js";
3
+ import { normalizeUrl } from "../utils/url-safety.js";
4
+ const DEFAULT_CRAWL_CONFIG = {
5
+ maxPages: 200,
6
+ maxDepth: 5,
7
+ concurrency: 3,
8
+ respectRobotsTxt: true,
9
+ crawlDelayMs: 500,
10
+ includeExternal: false,
11
+ };
12
+ function extractInternalLinks(html, baseUrl) {
13
+ const links = [];
14
+ const hrefRegex = /<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi;
15
+ let match;
16
+ while ((match = hrefRegex.exec(html)) !== null) {
17
+ try {
18
+ const resolved = new URL(match[1], baseUrl).href;
19
+ const base = new URL(baseUrl);
20
+ const resolvedUrl = new URL(resolved);
21
+ // Only include same-host, non-fragment, non-anchor links
22
+ if (resolvedUrl.hostname === base.hostname &&
23
+ resolvedUrl.protocol.startsWith("http") &&
24
+ !resolved.includes("#") &&
25
+ !resolved.includes("javascript:") &&
26
+ !resolved.includes("mailto:")) {
27
+ // Normalize: remove trailing slash, lowercase path
28
+ const normalized = normalizeUrl(resolved);
29
+ links.push(normalized);
30
+ }
31
+ }
32
+ catch {
33
+ // skip malformed URLs
34
+ }
35
+ }
36
+ // Deduplicate
37
+ return [...new Set(links)];
38
+ }
39
+ function parseRobotsTxtForDisallowed(content, userAgent) {
40
+ const lines = content.split("\n").map((l) => l.trim());
41
+ const disallowed = [];
42
+ let crawlDelay = null;
43
+ let currentUserAgents = [];
44
+ let inRelevantBlock = false;
45
+ for (const line of lines) {
46
+ if (!line || line.startsWith("#"))
47
+ continue;
48
+ const colonIdx = line.indexOf(":");
49
+ if (colonIdx === -1)
50
+ continue;
51
+ const key = line.slice(0, colonIdx).trim().toLowerCase();
52
+ const value = line.slice(colonIdx + 1).trim();
53
+ if (key === "user-agent") {
54
+ currentUserAgents.push(value.toLowerCase());
55
+ }
56
+ else if (inRelevantBlock || currentUserAgents.some((ua) => ua === "*" || ua === userAgent.toLowerCase())) {
57
+ inRelevantBlock = true;
58
+ if (key === "disallow") {
59
+ if (value)
60
+ disallowed.push(value);
61
+ }
62
+ else if (key === "crawl-delay") {
63
+ const delay = Number.parseFloat(value);
64
+ if (!Number.isNaN(delay))
65
+ crawlDelay = delay;
66
+ }
67
+ }
68
+ if (key !== "user-agent") {
69
+ if (!inRelevantBlock)
70
+ currentUserAgents = [];
71
+ }
72
+ }
73
+ return { disallowed, crawlDelay };
74
+ }
75
+ function isDisallowed(url, disallowedPaths) {
76
+ try {
77
+ const pathname = new URL(url).pathname;
78
+ for (const pattern of disallowedPaths) {
79
+ if (pattern === "/")
80
+ return true;
81
+ if (pattern && pathname.startsWith(pattern))
82
+ return true;
83
+ }
84
+ }
85
+ catch {
86
+ return true; // can't parse → block
87
+ }
88
+ return false;
89
+ }
90
+ export async function crawlSite(startUrl, config, crawlConfig) {
91
+ const cfg = { ...DEFAULT_CRAWL_CONFIG, ...crawlConfig };
92
+ const normalizedStart = normalizeUrl(startUrl);
93
+ const baseHost = new URL(normalizedStart).hostname;
94
+ // Fetch robots.txt
95
+ let disallowedPaths = [];
96
+ let robotsCrawlDelay = null;
97
+ if (cfg.respectRobotsTxt) {
98
+ try {
99
+ const robotsResult = await fetchRobotsTxt(normalizedStart, config);
100
+ if (robotsResult.statusCode === 200) {
101
+ const parsed = parseRobotsTxtForDisallowed(robotsResult.content, config.userAgent);
102
+ disallowedPaths = parsed.disallowed;
103
+ robotsCrawlDelay = parsed.crawlDelay;
104
+ }
105
+ }
106
+ catch {
107
+ // robots.txt fetch failed — proceed without restrictions
108
+ }
109
+ }
110
+ const delayMs = robotsCrawlDelay ? robotsCrawlDelay * 1000 : cfg.crawlDelayMs;
111
+ // BFS crawl
112
+ const visited = new Set();
113
+ const queue = [{ url: normalizedStart, depth: 0 }];
114
+ const pages = [];
115
+ const brokenLinks = [];
116
+ const errors = [];
117
+ const allDiscoveredLinks = new Set();
118
+ while (queue.length > 0 && visited.size < cfg.maxPages) {
119
+ // Process batch of concurrent pages
120
+ const batch = [];
121
+ while (batch.length < cfg.concurrency && queue.length > 0 && visited.size + batch.length < cfg.maxPages) {
122
+ const next = queue.shift();
123
+ if (visited.has(next.url))
124
+ continue;
125
+ if (isDisallowed(next.url, disallowedPaths))
126
+ continue;
127
+ batch.push(next);
128
+ }
129
+ if (batch.length === 0)
130
+ break;
131
+ const batchResults = await Promise.allSettled(batch.map(async ({ url, depth }) => {
132
+ const pageStart = Date.now();
133
+ try {
134
+ const fetchResult = await fetchPage(url, config);
135
+ const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
136
+ const internalLinks = extractInternalLinks(fetchResult.content, fetchResult.finalUrl);
137
+ return {
138
+ url: fetchResult.finalUrl,
139
+ depth,
140
+ statusCode: fetchResult.statusCode,
141
+ fetchResult,
142
+ pageData,
143
+ linksFound: internalLinks,
144
+ crawlDuration: Date.now() - pageStart,
145
+ };
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ errors.push(`Failed to crawl ${url}: ${msg}`);
150
+ return {
151
+ url,
152
+ depth,
153
+ statusCode: 0,
154
+ fetchResult: null,
155
+ pageData: null,
156
+ linksFound: [],
157
+ crawlDuration: Date.now() - pageStart,
158
+ error: msg,
159
+ };
160
+ }
161
+ }));
162
+ for (const result of batchResults) {
163
+ if (result.status === "rejected") {
164
+ errors.push(`Batch crawl error: ${String(result.reason)}`);
165
+ continue;
166
+ }
167
+ const page = result.value;
168
+ visited.add(page.url);
169
+ pages.push(page);
170
+ // Check for broken external links from page data
171
+ if (page.pageData) {
172
+ for (const link of page.pageData.links) {
173
+ if (link.type === "external")
174
+ continue;
175
+ allDiscoveredLinks.add(link.href);
176
+ // Internal links we haven't visited yet — add to queue
177
+ const normalized = normalizeUrl(link.href);
178
+ if (!visited.has(normalized) && normalized !== page.url) {
179
+ try {
180
+ const linkUrl = new URL(normalized);
181
+ if (linkUrl.hostname === baseHost && page.depth < cfg.maxDepth) {
182
+ queue.push({ url: normalized, depth: page.depth + 1 });
183
+ }
184
+ }
185
+ catch {
186
+ // skip
187
+ }
188
+ }
189
+ }
190
+ }
191
+ // Delay between batches
192
+ if (delayMs > 0) {
193
+ await sleep(delayMs);
194
+ }
195
+ }
196
+ }
197
+ // Detect broken links — check pages with non-200 status
198
+ for (const page of pages) {
199
+ if (page.statusCode >= 400) {
200
+ brokenLinks.push({
201
+ sourcePage: normalizedStart,
202
+ targetUrl: page.url,
203
+ statusCode: page.statusCode,
204
+ linkText: "",
205
+ });
206
+ }
207
+ }
208
+ // Detect duplicate content
209
+ const duplicateGroups = detectDuplicates(pages);
210
+ // Orphan pages = pages only discovered via crawling that weren't linked from any other page
211
+ const linkedPages = new Set();
212
+ for (const page of pages) {
213
+ for (const link of page.linksFound) {
214
+ linkedPages.add(link);
215
+ }
216
+ }
217
+ const orphanPages = pages
218
+ .filter((p) => p.depth === 0 && !linkedPages.has(p.url))
219
+ .map((p) => p.url);
220
+ // Build link graph
221
+ const linkGraph = pages.map((p) => ({
222
+ url: p.url,
223
+ internalInlinks: pages.reduce((count, other) => count + (other.linksFound.includes(p.url) ? 1 : 0), 0),
224
+ internalOutlinks: p.linksFound.length,
225
+ externalOutlinks: p.pageData ? p.pageData.externalLinkCount : 0,
226
+ depth: p.depth,
227
+ statusCode: p.statusCode,
228
+ }));
229
+ return {
230
+ baseUrl: normalizedStart,
231
+ pagesCrawled: pages.length,
232
+ totalPagesFound: allDiscoveredLinks.size,
233
+ maxDepthReached: pages.reduce((max, p) => Math.max(max, p.depth), 0),
234
+ brokenLinks,
235
+ duplicateGroups,
236
+ orphanPages,
237
+ pages,
238
+ linkGraph,
239
+ errors,
240
+ };
241
+ }
242
+ function detectDuplicates(pages) {
243
+ const groups = [];
244
+ const titleMap = new Map();
245
+ const descMap = new Map();
246
+ const h1Map = new Map();
247
+ for (const page of pages) {
248
+ if (!page.pageData)
249
+ continue;
250
+ const title = page.pageData.title?.trim().toLowerCase();
251
+ if (title) {
252
+ if (!titleMap.has(title))
253
+ titleMap.set(title, []);
254
+ titleMap.get(title).push(page.url);
255
+ }
256
+ const desc = page.pageData.metaDescription?.trim().toLowerCase();
257
+ if (desc) {
258
+ if (!descMap.has(desc))
259
+ descMap.set(desc, []);
260
+ descMap.get(desc).push(page.url);
261
+ }
262
+ const h1 = page.pageData.headings
263
+ .filter((h) => h.level === 1)
264
+ .map((h) => h.text.trim().toLowerCase())
265
+ .join(" | ");
266
+ if (h1) {
267
+ if (!h1Map.has(h1))
268
+ h1Map.set(h1, []);
269
+ h1Map.get(h1).push(page.url);
270
+ }
271
+ }
272
+ for (const [title, urls] of titleMap) {
273
+ if (urls.length > 1)
274
+ groups.push({ type: "title", value: title, pageUrls: urls });
275
+ }
276
+ for (const [desc, urls] of descMap) {
277
+ if (urls.length > 1)
278
+ groups.push({ type: "description", value: desc, pageUrls: urls });
279
+ }
280
+ for (const [h1, urls] of h1Map) {
281
+ if (urls.length > 1)
282
+ groups.push({ type: "h1", value: h1, pageUrls: urls });
283
+ }
284
+ return groups;
285
+ }
286
+ function sleep(ms) {
287
+ return new Promise((resolve) => setTimeout(resolve, ms));
288
+ }
@@ -0,0 +1,9 @@
1
+ import type { KeywordAnalysisResult } from "../types.js";
2
+ export declare function analyzeKeywords(text: string, url?: string): KeywordAnalysisResult;
3
+ export declare function analyzeKeywordDensity(text: string, targetKeywords: string[]): Array<{
4
+ keyword: string;
5
+ count: number;
6
+ density: number;
7
+ verdict: string;
8
+ }>;
9
+ //# sourceMappingURL=keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keywords.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/keywords.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAe,MAAM,aAAa,CAAC;AA4GtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAgEjF;AAeD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,EAAE,GACvB,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkB7E"}