@aexol/spectral 0.9.126 → 0.9.128
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/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +5 -0
- package/dist/extensions/seo/analyzers/content.d.ts +29 -6
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/content.js +243 -8
- package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/crawler.js +133 -7
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/drift.js +86 -29
- package/dist/extensions/seo/analyzers/images.d.ts +10 -4
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/images.js +153 -5
- package/dist/extensions/seo/analyzers/performance.d.ts +3 -3
- package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/performance.js +77 -6
- package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/readability.js +17 -9
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/schema.js +99 -10
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/sitemap.js +57 -16
- package/dist/extensions/seo/analyzers/technical.d.ts +0 -7
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/technical.js +408 -37
- package/dist/extensions/seo/config.d.ts +2 -0
- package/dist/extensions/seo/config.d.ts.map +1 -1
- package/dist/extensions/seo/config.js +2 -0
- package/dist/extensions/seo/tools/batch.js +1 -1
- package/dist/extensions/seo/tools/content.d.ts.map +1 -1
- package/dist/extensions/seo/tools/content.js +5 -3
- package/dist/extensions/seo/tools/full-audit.d.ts +1 -1
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +391 -66
- package/dist/extensions/seo/tools/helpers.d.ts +15 -4
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -1
- package/dist/extensions/seo/tools/helpers.js +57 -6
- package/dist/extensions/seo/tools/performance.d.ts.map +1 -1
- package/dist/extensions/seo/tools/performance.js +17 -11
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +513 -47
- package/dist/extensions/seo/types.d.ts +295 -3
- package/dist/extensions/seo/types.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.d.ts +4 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.js +77 -11
- package/dist/extensions/seo/utils/parser.d.ts +3 -1
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -1
- package/dist/extensions/seo/utils/parser.js +105 -25
- package/dist/extensions/seo/utils/renderer.d.ts +6 -1
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -1
- package/dist/extensions/seo/utils/renderer.js +207 -1
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
- package/dist/extensions/seo/utils/url-safety.js +19 -3
- package/dist/extensions/web/tools/fetch.js +2 -2
- package/dist/memory/tools/compact-context.d.ts.map +1 -1
- package/dist/memory/tools/compact-context.js +10 -3
- package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
- package/dist/sdk/agent-core/agent-loop.js +87 -4
- package/dist/sdk/agent-core/types.d.ts +1 -0
- package/dist/sdk/agent-core/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.d.ts +0 -7
- package/dist/sdk/ai/utils/validation.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.js +21 -1
- package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/runner.js +15 -1
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts +7 -2
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/apply-patch.js +219 -41
- package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts +30 -0
- package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/atomic-write.js +42 -0
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts +10 -4
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit-diff.js +44 -24
- package/dist/sdk/coding-agent/core/tools/edit.d.ts +6 -0
- package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit.js +31 -5
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts +9 -3
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.js +42 -3
- package/dist/sdk/coding-agent/core/tools/read.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/read.js +79 -47
- package/dist/sdk/coding-agent/core/tools/truncate.d.ts +4 -2
- package/dist/sdk/coding-agent/core/tools/truncate.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/truncate.js +43 -23
- package/dist/sdk/coding-agent/core/tools/write.d.ts +5 -8
- package/dist/sdk/coding-agent/core/tools/write.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/write.js +29 -11
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAIX,SAAS,EAET,MAAM,4BAA4B,CAAC;AAUpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,KAAK,WAAW,EAAmC,MAAM,aAAa,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOrD,OAAO,KAAK,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AA4F7G,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpG;AAED,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,GAC/C,MAAM,CAKR;AAiHD;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,CAK3D;AAED,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,GACnB,SAAS,EAAE,CAqCb;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAIX,SAAS,EAET,MAAM,4BAA4B,CAAC;AAUpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,KAAK,WAAW,EAAmC,MAAM,aAAa,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOrD,OAAO,KAAK,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AA4F7G,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpG;AAED,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,GAC/C,MAAM,CAKR;AAiHD;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,CAK3D;AAED,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,GACnB,SAAS,EAAE,CAqCb;AA4aD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;CAAE,GACpF,IAAI,CAgZN"}
|
package/dist/agent/index.js
CHANGED
|
@@ -494,6 +494,11 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
494
494
|
if (event.type === "tool_execution_end" && event.isError) {
|
|
495
495
|
emitUpdate();
|
|
496
496
|
}
|
|
497
|
+
if (event.type === "agent_end" && event.error) {
|
|
498
|
+
hadError = true;
|
|
499
|
+
currentResult.errorMessage = event.error;
|
|
500
|
+
currentResult.stderr = event.error;
|
|
501
|
+
}
|
|
497
502
|
}
|
|
498
503
|
// Drain the stream result (already consumed by iteration, but ensure cleanup)
|
|
499
504
|
try {
|
|
@@ -5,12 +5,35 @@
|
|
|
5
5
|
* analysis: filler phrase detection, AI-pattern detection, information density,
|
|
6
6
|
* and repetition scoring. No external API calls.
|
|
7
7
|
*/
|
|
8
|
-
import type { ContentQualityResult } from "../types.js";
|
|
8
|
+
import type { ContentQualityResult, HeadingInfo, PageData, SchemaBlock } from "../types.js";
|
|
9
|
+
/** Split text into sentences, respecting common abbreviations. */
|
|
10
|
+
export declare function splitSentences(text: string): string[];
|
|
11
|
+
/** Calculate Flesch-Kincaid Grade Level and related readability metrics. */
|
|
12
|
+
export declare function calculateReadability(text: string): {
|
|
13
|
+
fleschKincaid: number;
|
|
14
|
+
sentenceCount: number;
|
|
15
|
+
avgSentenceLength: number;
|
|
16
|
+
avgWordLength: number;
|
|
17
|
+
};
|
|
18
|
+
/** Extract named entities from text using proper noun heuristics. */
|
|
19
|
+
export declare function extractEntities(text: string, schemaBlocks?: SchemaBlock[]): string[];
|
|
20
|
+
/** Check whether heading keywords appear in body text. */
|
|
21
|
+
export declare function headingContentAlignment(headings: HeadingInfo[], bodyText: string, stopWords?: Set<string>): {
|
|
22
|
+
score: number;
|
|
23
|
+
missingKeywords: string[];
|
|
24
|
+
};
|
|
25
|
+
/** Evaluate heading structure depth and completeness. */
|
|
26
|
+
export declare function calculateSemanticDepth(headings: HeadingInfo[]): {
|
|
27
|
+
score: number;
|
|
28
|
+
hasH1: boolean;
|
|
29
|
+
hasH2: boolean;
|
|
30
|
+
hasH3: boolean;
|
|
31
|
+
headingCount: number;
|
|
32
|
+
};
|
|
9
33
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* All scores are 0-100 where higher is better (except filler and AI pattern
|
|
13
|
-
* scores, where higher means MORE problematic content).
|
|
34
|
+
* Detect language using word-frequency profiles of common function words.
|
|
35
|
+
* Returns the ISO 639-1 language code of the best match, or null if confidence is too low.
|
|
14
36
|
*/
|
|
15
|
-
export declare function
|
|
37
|
+
export declare function detectLanguage(text: string): string | null;
|
|
38
|
+
export declare function analyzeContentQuality(text: string, url?: string, pageData?: PageData, customStopWords?: string[]): ContentQualityResult;
|
|
16
39
|
//# sourceMappingURL=content.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/content.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/content.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,QAAQ,EAAqB,WAAW,EAAE,MAAM,aAAa,CAAC;AAqJ/G,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAcrD;AAaD,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB,CA2BA;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,WAAW,EAAE,GAAG,MAAM,EAAE,CA0BpF;AAED,0DAA0D;AAC1D,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACtB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAA;CAAE,CAsB9C;AAED,yDAAyD;AACzD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB,CAiBA;AAmBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgC1D;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,GAAG,CAAC,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,QAAQ,EACnB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,oBAAoB,CAoFtB"}
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import { analyzeReadability } from "./readability.js";
|
|
2
|
+
/** Common stop words — filtered during tokenization to improve signal. */
|
|
3
|
+
const STOP_WORDS = new Set([
|
|
4
|
+
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
5
|
+
"of", "with", "by", "from", "up", "about", "into", "through", "during",
|
|
6
|
+
"before", "after", "above", "below", "between", "out", "off", "over",
|
|
7
|
+
"under", "again", "further", "then", "once", "here", "there", "when",
|
|
8
|
+
"where", "why", "how", "all", "both", "each", "few", "more", "most",
|
|
9
|
+
"other", "some", "such", "no", "nor", "not", "only", "own", "same",
|
|
10
|
+
"so", "than", "too", "very", "s", "t", "can", "will", "just", "don",
|
|
11
|
+
"should", "now", "is", "are", "was", "were", "be", "been", "being",
|
|
12
|
+
"have", "has", "had", "having", "do", "does", "did", "doing", "it",
|
|
13
|
+
"its", "this", "that", "these", "those", "i", "me", "my", "we", "our",
|
|
14
|
+
"you", "your", "he", "she", "they", "them", "their", "what", "which",
|
|
15
|
+
"who", "whom", "would", "could", "may", "might", "shall", "must",
|
|
16
|
+
]);
|
|
1
17
|
/** Common filler phrases that add no information value. */
|
|
2
18
|
const FILLER_PHRASES = [
|
|
3
19
|
"in today's fast-paced world",
|
|
@@ -72,11 +88,19 @@ const AI_PATTERNS = [
|
|
|
72
88
|
"transform your",
|
|
73
89
|
"redefine the way",
|
|
74
90
|
];
|
|
75
|
-
|
|
91
|
+
/** Common English abbreviations that should not trigger sentence splits. */
|
|
92
|
+
const ABBREVIATIONS = new Set([
|
|
93
|
+
"mr", "mrs", "ms", "dr", "prof", "sr", "jr", "st", "ave", "blvd",
|
|
94
|
+
"etc", "vs", "approx", "dept", "est", "eg", "ie", "inc", "ltd",
|
|
95
|
+
"co", "corp", "jan", "feb", "mar", "apr", "jun", "jul", "aug",
|
|
96
|
+
"sep", "oct", "nov", "dec",
|
|
97
|
+
]);
|
|
98
|
+
function tokenize(text, stopWords) {
|
|
99
|
+
const sw = stopWords ?? STOP_WORDS;
|
|
76
100
|
return text
|
|
77
101
|
.toLowerCase()
|
|
78
102
|
.split(/[^a-z0-9]+/)
|
|
79
|
-
.filter((t) => t.length > 0);
|
|
103
|
+
.filter((t) => t.length > 0 && !sw.has(t));
|
|
80
104
|
}
|
|
81
105
|
function countPatternMatches(text, patterns) {
|
|
82
106
|
const lower = text.toLowerCase();
|
|
@@ -95,7 +119,6 @@ function countPatternMatches(text, patterns) {
|
|
|
95
119
|
function calculateRepetition(tokens) {
|
|
96
120
|
if (tokens.length === 0)
|
|
97
121
|
return 0;
|
|
98
|
-
// Count trigram repetitions.
|
|
99
122
|
const trigrams = new Map();
|
|
100
123
|
for (let i = 0; i < tokens.length - 2; i++) {
|
|
101
124
|
const trigram = `${tokens[i]} ${tokens[i + 1]} ${tokens[i + 2]}`;
|
|
@@ -112,21 +135,204 @@ function calculateRepetition(tokens) {
|
|
|
112
135
|
function calculateInformationDensity(text, tokens) {
|
|
113
136
|
if (tokens.length === 0)
|
|
114
137
|
return 0;
|
|
115
|
-
// Count entities (capitalized words) and numbers.
|
|
116
138
|
const entities = (text.match(/\b[A-Z][a-z]+\b/g) ?? []).length;
|
|
117
139
|
const numbers = (text.match(/\b\d+([.,]\d+)?\b/g) ?? []).length;
|
|
118
140
|
const informative = entities + numbers;
|
|
119
|
-
// Density = informative tokens / total tokens, scaled 0-100.
|
|
120
141
|
return Math.min(100, Math.round((informative / tokens.length) * 1000));
|
|
121
142
|
}
|
|
143
|
+
/** Split text into sentences, respecting common abbreviations. */
|
|
144
|
+
export function splitSentences(text) {
|
|
145
|
+
if (!text)
|
|
146
|
+
return [];
|
|
147
|
+
// Replace known abbreviations with placeholders to avoid false splits.
|
|
148
|
+
let processed = text;
|
|
149
|
+
for (const abbr of ABBREVIATIONS) {
|
|
150
|
+
const escaped = abbr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
151
|
+
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
152
|
+
processed = processed.replace(re, (match) => match.replace(".", "##DOT##"));
|
|
153
|
+
}
|
|
154
|
+
// Split on .!? followed by whitespace and uppercase letter (or end of string).
|
|
155
|
+
const sentences = processed.split(/(?<=[.!?])\s+(?=[A-Z])/);
|
|
156
|
+
return sentences
|
|
157
|
+
.map((s) => s.replace(/##DOT##/g, ".").trim())
|
|
158
|
+
.filter((s) => s.length > 0);
|
|
159
|
+
}
|
|
160
|
+
/** Count syllables in a word using a simple vowel-group heuristic. */
|
|
161
|
+
function countSyllables(word) {
|
|
162
|
+
const cleaned = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
163
|
+
if (cleaned.length <= 3)
|
|
164
|
+
return 1;
|
|
165
|
+
const vowelGroups = cleaned.match(/[aeiouy]+/g);
|
|
166
|
+
let count = vowelGroups ? vowelGroups.length : 1;
|
|
167
|
+
// Subtract silent 'e' at end.
|
|
168
|
+
if (cleaned.endsWith("e") && count > 1)
|
|
169
|
+
count--;
|
|
170
|
+
return Math.max(1, count);
|
|
171
|
+
}
|
|
172
|
+
/** Calculate Flesch-Kincaid Grade Level and related readability metrics. */
|
|
173
|
+
export function calculateReadability(text) {
|
|
174
|
+
const sentences = splitSentences(text);
|
|
175
|
+
const sentenceCount = sentences.length;
|
|
176
|
+
if (sentenceCount === 0) {
|
|
177
|
+
return { fleschKincaid: 0, sentenceCount: 0, avgSentenceLength: 0, avgWordLength: 0 };
|
|
178
|
+
}
|
|
179
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
180
|
+
const wordCount = words.length;
|
|
181
|
+
if (wordCount === 0) {
|
|
182
|
+
return { fleschKincaid: 0, sentenceCount, avgSentenceLength: 0, avgWordLength: 0 };
|
|
183
|
+
}
|
|
184
|
+
const avgSentenceLength = wordCount / sentenceCount;
|
|
185
|
+
let totalSyllables = 0;
|
|
186
|
+
let totalLetters = 0;
|
|
187
|
+
for (const word of words) {
|
|
188
|
+
totalSyllables += countSyllables(word);
|
|
189
|
+
totalLetters += word.replace(/[^a-zA-Z]/g, "").length;
|
|
190
|
+
}
|
|
191
|
+
const avgWordLength = totalLetters / wordCount;
|
|
192
|
+
// Flesch-Kincaid Grade Level = 0.39*(words/sentences) + 11.8*(syllables/words) - 15.59
|
|
193
|
+
const fleschKincaid = 0.39 * avgSentenceLength + 11.8 * (totalSyllables / wordCount) - 15.59;
|
|
194
|
+
return {
|
|
195
|
+
fleschKincaid: Math.round(fleschKincaid * 10) / 10,
|
|
196
|
+
sentenceCount,
|
|
197
|
+
avgSentenceLength: Math.round(avgSentenceLength * 10) / 10,
|
|
198
|
+
avgWordLength: Math.round(avgWordLength * 10) / 10,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Extract named entities from text using proper noun heuristics. */
|
|
202
|
+
export function extractEntities(text, schemaBlocks) {
|
|
203
|
+
const entities = new Set();
|
|
204
|
+
// Extract consecutive capitalized words (potential proper nouns).
|
|
205
|
+
const properNouns = text.match(/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/g) ?? [];
|
|
206
|
+
for (const noun of properNouns) {
|
|
207
|
+
// Only keep multi-word or clearly proper noun single words (not sentence starters).
|
|
208
|
+
if (noun.includes(" ") || noun.length > 3) {
|
|
209
|
+
entities.add(noun);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Cross-reference with schema @type values.
|
|
213
|
+
if (schemaBlocks) {
|
|
214
|
+
for (const block of schemaBlocks) {
|
|
215
|
+
if (block.type && block.type !== "Unknown" && block.type !== "Invalid" && block.type !== "InvalidJSON") {
|
|
216
|
+
entities.add(block.type);
|
|
217
|
+
}
|
|
218
|
+
// Also check parsed schema for name properties.
|
|
219
|
+
if (block.parsed && typeof block.parsed === "object") {
|
|
220
|
+
const parsed = block.parsed;
|
|
221
|
+
if (typeof parsed.name === "string" && parsed.name.length > 0) {
|
|
222
|
+
entities.add(parsed.name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return [...entities];
|
|
228
|
+
}
|
|
229
|
+
/** Check whether heading keywords appear in body text. */
|
|
230
|
+
export function headingContentAlignment(headings, bodyText, stopWords) {
|
|
231
|
+
const sw = stopWords ?? STOP_WORDS;
|
|
232
|
+
const lowerBody = bodyText.toLowerCase();
|
|
233
|
+
// Extract meaningful keywords from H1/H2 text (words 4+ chars, not stop words).
|
|
234
|
+
const headingKeywords = new Set();
|
|
235
|
+
for (const h of headings) {
|
|
236
|
+
if (h.level > 2)
|
|
237
|
+
continue; // Only H1/H2.
|
|
238
|
+
const words = h.text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4 && !sw.has(w));
|
|
239
|
+
for (const w of words)
|
|
240
|
+
headingKeywords.add(w);
|
|
241
|
+
}
|
|
242
|
+
if (headingKeywords.size === 0)
|
|
243
|
+
return { score: 100, missingKeywords: [] };
|
|
244
|
+
const missingKeywords = [];
|
|
245
|
+
let found = 0;
|
|
246
|
+
for (const kw of headingKeywords) {
|
|
247
|
+
if (lowerBody.includes(kw)) {
|
|
248
|
+
found++;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
missingKeywords.push(kw);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const score = Math.round((found / headingKeywords.size) * 100);
|
|
255
|
+
return { score, missingKeywords };
|
|
256
|
+
}
|
|
257
|
+
/** Evaluate heading structure depth and completeness. */
|
|
258
|
+
export function calculateSemanticDepth(headings) {
|
|
259
|
+
let hasH1 = false;
|
|
260
|
+
let hasH2 = false;
|
|
261
|
+
let hasH3 = false;
|
|
262
|
+
let headingCount = 0;
|
|
263
|
+
for (const h of headings) {
|
|
264
|
+
headingCount++;
|
|
265
|
+
if (h.level === 1)
|
|
266
|
+
hasH1 = true;
|
|
267
|
+
if (h.level === 2)
|
|
268
|
+
hasH2 = true;
|
|
269
|
+
if (h.level === 3)
|
|
270
|
+
hasH3 = true;
|
|
271
|
+
}
|
|
272
|
+
let score = 0;
|
|
273
|
+
if (hasH1)
|
|
274
|
+
score += 40;
|
|
275
|
+
if (hasH2)
|
|
276
|
+
score += 35;
|
|
277
|
+
if (hasH3)
|
|
278
|
+
score += 15;
|
|
279
|
+
if (headingCount >= 5)
|
|
280
|
+
score += 10;
|
|
281
|
+
return { score, hasH1, hasH2, hasH3, headingCount };
|
|
282
|
+
}
|
|
122
283
|
/**
|
|
123
284
|
* Analyze content quality of a text string.
|
|
124
285
|
*
|
|
125
286
|
* All scores are 0-100 where higher is better (except filler and AI pattern
|
|
126
287
|
* scores, where higher means MORE problematic content).
|
|
127
288
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
289
|
+
const LANGUAGE_PROFILES = {
|
|
290
|
+
en: ["the", "be", "to", "of", "and", "a", "in", "that", "have", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they", "we", "her", "she", "or", "an", "will", "my", "one", "all", "would", "there", "their"],
|
|
291
|
+
pl: ["i", "w", "się", "na", "nie", "to", "jest", "że", "z", "do", "o", "a", "co", "po", "tak", "jak", "ale", "mnie", "już", "tego", "czy", "jej", "był", "bardzo", "dla", "go", "przy", "przed", "nad", "pod"],
|
|
292
|
+
de: ["der", "die", "und", "in", "zu", "den", "das", "nicht", "von", "sie", "ist", "des", "sich", "mit", "dem", "dass", "er", "es", "ein", "ich", "auf", "so", "eine", "auch", "als", "an", "nach", "wie", "im", "für"],
|
|
293
|
+
fr: ["de", "la", "le", "et", "les", "des", "en", "un", "une", "du", "que", "pas", "pour", "dans", "qui", "par", "sur", "au", "avec", "ne", "ce", "plus", "se", "son", "aux", "dont", "comme", "tout", "cette", "même"],
|
|
294
|
+
es: ["de", "que", "el", "en", "y", "a", "los", "se", "del", "las", "un", "por", "con", "no", "una", "su", "para", "es", "al", "lo", "como", "más", "o", "pero", "sus", "le", "ha", "me", "si", "sin"],
|
|
295
|
+
it: ["di", "che", "e", "la", "il", "un", "a", "per", "in", "una", "con", "non", "si", "le", "del", "da", "più", "al", "ma", "gli", "ad", "anche", "o", "cui", "suo", "tra", "mio", "questa", "quello", "dalla"],
|
|
296
|
+
pt: ["de", "que", "e", "do", "a", "o", "não", "um", "da", "se", "com", "uma", "os", "no", "me", "por", "as", "dos", "mas", "ele", "na", "ao", "para", "bem", "seu", "isso", "pela", "lhe", "qual", "já"],
|
|
297
|
+
nl: ["de", "van", "een", "het", "dat", "in", "en", "is", "niet", "op", "te", "voor", "zijn", "met", "die", "aan", "er", "als", "ook", "om", "dan", "uit", "bij", "of", "kan", "nog", "naar", "al", "worden", "door"],
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Detect language using word-frequency profiles of common function words.
|
|
301
|
+
* Returns the ISO 639-1 language code of the best match, or null if confidence is too low.
|
|
302
|
+
*/
|
|
303
|
+
export function detectLanguage(text) {
|
|
304
|
+
const words = text.toLowerCase().split(/[^a-zà-ÿ]+/).filter(w => w.length >= 2);
|
|
305
|
+
const scores = {};
|
|
306
|
+
for (const [lang, signalWords] of Object.entries(LANGUAGE_PROFILES)) {
|
|
307
|
+
const signalSet = new Set(signalWords);
|
|
308
|
+
let count = 0;
|
|
309
|
+
for (const word of words) {
|
|
310
|
+
if (signalSet.has(word))
|
|
311
|
+
count++;
|
|
312
|
+
}
|
|
313
|
+
scores[lang] = count;
|
|
314
|
+
}
|
|
315
|
+
let bestLang = null;
|
|
316
|
+
let bestScore = 0;
|
|
317
|
+
let secondBest = 0;
|
|
318
|
+
for (const [lang, score] of Object.entries(scores)) {
|
|
319
|
+
if (score > bestScore) {
|
|
320
|
+
secondBest = bestScore;
|
|
321
|
+
bestScore = score;
|
|
322
|
+
bestLang = lang;
|
|
323
|
+
}
|
|
324
|
+
else if (score > secondBest) {
|
|
325
|
+
secondBest = score;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (bestScore < 3 || (secondBest > 0 && bestScore < secondBest * 2)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return bestLang;
|
|
332
|
+
}
|
|
333
|
+
export function analyzeContentQuality(text, url, pageData, customStopWords) {
|
|
334
|
+
const effectiveStopWords = new Set([...STOP_WORDS, ...((customStopWords ?? []).map(w => w.toLowerCase()))]);
|
|
335
|
+
const tokens = tokenize(text, effectiveStopWords);
|
|
130
336
|
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
131
337
|
const uniqueTokens = new Set(tokens).size;
|
|
132
338
|
// Filler phrase detection.
|
|
@@ -157,7 +363,7 @@ export function analyzeContentQuality(text, url) {
|
|
|
157
363
|
flags.push("low-information-density");
|
|
158
364
|
if (wordCount < 300)
|
|
159
365
|
flags.push("thin-content");
|
|
160
|
-
|
|
366
|
+
const result = {
|
|
161
367
|
url,
|
|
162
368
|
wordCount,
|
|
163
369
|
tokens: tokens.length,
|
|
@@ -170,5 +376,34 @@ export function analyzeContentQuality(text, url) {
|
|
|
170
376
|
flags,
|
|
171
377
|
fillerMatches: filler.matches,
|
|
172
378
|
aiPatternMatches: aiPatterns.matches,
|
|
379
|
+
disclaimer: "Note: This analysis measures content quality heuristics only. It does not measure search rankings, traffic, or search volume data.",
|
|
173
380
|
};
|
|
381
|
+
// Enriched analysis when pageData is available.
|
|
382
|
+
if (pageData) {
|
|
383
|
+
// Readability.
|
|
384
|
+
const readabilityResult = analyzeReadability(text, url);
|
|
385
|
+
result.readability = readabilityResult;
|
|
386
|
+
result.sentenceCount = readabilityResult.sentenceCount;
|
|
387
|
+
result.avgSentenceLength = readabilityResult.sentenceCount > 0
|
|
388
|
+
? Math.round((result.wordCount / readabilityResult.sentenceCount) * 10) / 10
|
|
389
|
+
: 0;
|
|
390
|
+
const totalLetters = text.replace(/[^a-zA-Z]/g, "").length;
|
|
391
|
+
result.avgWordLength = result.wordCount > 0
|
|
392
|
+
? Math.round((totalLetters / result.wordCount) * 10) / 10
|
|
393
|
+
: 0;
|
|
394
|
+
result.readabilityFleschKincaid = readabilityResult.fleschKincaid;
|
|
395
|
+
// Heading-content alignment.
|
|
396
|
+
if (pageData.headings && pageData.headings.length > 0) {
|
|
397
|
+
result.headingAlignment = headingContentAlignment(pageData.headings, text, effectiveStopWords);
|
|
398
|
+
}
|
|
399
|
+
// Semantic depth.
|
|
400
|
+
if (pageData.headings) {
|
|
401
|
+
result.semanticDepth = calculateSemanticDepth(pageData.headings);
|
|
402
|
+
}
|
|
403
|
+
// Entity extraction.
|
|
404
|
+
result.entities = extractEntities(text, pageData.schemaBlocks);
|
|
405
|
+
// Language.
|
|
406
|
+
result.language = pageData.language ?? detectLanguage(text) ?? undefined;
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
174
409
|
}
|
|
@@ -1 +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,EAEV,WAAW,EACX,WAAW,EAGZ,MAAM,aAAa,CAAC;
|
|
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,EAEV,WAAW,EACX,WAAW,EAGZ,MAAM,aAAa,CAAC;AAsKrB,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GACjC,OAAO,CAAC,WAAW,CAAC,CAkOtB"}
|
|
@@ -30,12 +30,46 @@ function urlIdentity(url) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
function extractInternalLinks(html, baseUrl) {
|
|
33
|
-
const
|
|
33
|
+
const internalLinks = [];
|
|
34
|
+
const canonicalUrls = [];
|
|
35
|
+
const paginationUrls = [];
|
|
36
|
+
let nofollowCount = 0;
|
|
37
|
+
let sponsoredCount = 0;
|
|
38
|
+
let ugcCount = 0;
|
|
39
|
+
// Extract <link rel="canonical"> and <link rel="next">/<link rel="prev">
|
|
40
|
+
const linkTagRegex = /<link\s[^>]*\/?>/gi;
|
|
41
|
+
let linkMatch;
|
|
42
|
+
while ((linkMatch = linkTagRegex.exec(html)) !== null) {
|
|
43
|
+
const tag = linkMatch[0];
|
|
44
|
+
const relMatch = /rel=["']([^"']+)["']/i.exec(tag);
|
|
45
|
+
const hrefMatch = /href=["']([^"']+)["']/i.exec(tag);
|
|
46
|
+
if (!relMatch || !hrefMatch)
|
|
47
|
+
continue;
|
|
48
|
+
const rel = relMatch[1].toLowerCase();
|
|
49
|
+
const href = hrefMatch[1];
|
|
50
|
+
try {
|
|
51
|
+
const resolved = new URL(href, baseUrl).href;
|
|
52
|
+
const base = new URL(baseUrl);
|
|
53
|
+
const resolvedUrl = new URL(resolved);
|
|
54
|
+
if (resolvedUrl.hostname === base.hostname && resolvedUrl.protocol.startsWith("http")) {
|
|
55
|
+
const normalized = normalizeUrl(resolved);
|
|
56
|
+
if (rel === "canonical")
|
|
57
|
+
canonicalUrls.push(normalized);
|
|
58
|
+
if (rel === "next" || rel === "prev")
|
|
59
|
+
paginationUrls.push(normalized);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Extract <a href="..."> links with rel attributes.
|
|
34
66
|
const hrefRegex = /<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
35
67
|
let match;
|
|
36
68
|
while ((match = hrefRegex.exec(html)) !== null) {
|
|
69
|
+
const tag = match[0];
|
|
70
|
+
const href = match[1];
|
|
37
71
|
try {
|
|
38
|
-
const resolved = new URL(
|
|
72
|
+
const resolved = new URL(href, baseUrl).href;
|
|
39
73
|
const base = new URL(baseUrl);
|
|
40
74
|
const resolvedUrl = new URL(resolved);
|
|
41
75
|
if (resolvedUrl.hostname === base.hostname &&
|
|
@@ -45,14 +79,30 @@ function extractInternalLinks(html, baseUrl) {
|
|
|
45
79
|
!resolved.includes("mailto:") &&
|
|
46
80
|
!resolved.includes("/cdn-cgi/")) {
|
|
47
81
|
const normalized = normalizeUrl(resolved);
|
|
48
|
-
|
|
82
|
+
const relMatch = /rel=["']([^"']+)["']/i.exec(tag);
|
|
83
|
+
const relVals = relMatch ? relMatch[1].toLowerCase().split(/\s+/) : [];
|
|
84
|
+
if (relVals.includes("nofollow"))
|
|
85
|
+
nofollowCount++;
|
|
86
|
+
if (relVals.includes("sponsored"))
|
|
87
|
+
sponsoredCount++;
|
|
88
|
+
if (relVals.includes("ugc"))
|
|
89
|
+
ugcCount++;
|
|
90
|
+
if (!relVals.includes("nofollow")) {
|
|
91
|
+
internalLinks.push(normalized);
|
|
92
|
+
}
|
|
49
93
|
}
|
|
50
94
|
}
|
|
51
95
|
catch {
|
|
52
|
-
// skip malformed hrefs
|
|
53
96
|
}
|
|
54
97
|
}
|
|
55
|
-
return
|
|
98
|
+
return {
|
|
99
|
+
internalLinks: [...new Set(internalLinks)],
|
|
100
|
+
canonicalUrls: [...new Set(canonicalUrls)],
|
|
101
|
+
paginationUrls: [...new Set(paginationUrls)],
|
|
102
|
+
nofollowCount,
|
|
103
|
+
sponsoredCount,
|
|
104
|
+
ugcCount,
|
|
105
|
+
};
|
|
56
106
|
}
|
|
57
107
|
function parseRobotsTxtForDisallowed(content, userAgent) {
|
|
58
108
|
const lines = content.split("\n").map((l) => l.trim());
|
|
@@ -138,6 +188,12 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
138
188
|
const brokenLinks = [];
|
|
139
189
|
const errors = [];
|
|
140
190
|
const allDiscoveredLinks = new Set();
|
|
191
|
+
const canonicalUrlSet = new Set();
|
|
192
|
+
const redirectingInternalLinks = [];
|
|
193
|
+
let nofollowLinks = 0;
|
|
194
|
+
let sponsoredLinks = 0;
|
|
195
|
+
let ugcLinks = 0;
|
|
196
|
+
let paginationLinks = 0;
|
|
141
197
|
while (queue.length > 0 &&
|
|
142
198
|
visitedIdentities.size < cfg.maxPages) {
|
|
143
199
|
const batch = [];
|
|
@@ -159,15 +215,27 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
159
215
|
try {
|
|
160
216
|
const fetchResult = await fetchPage(url, config);
|
|
161
217
|
const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
162
|
-
const
|
|
218
|
+
const extracted = extractInternalLinks(fetchResult.content, fetchResult.finalUrl);
|
|
219
|
+
// Detect 3xx redirects to internal targets.
|
|
220
|
+
if (fetchResult.statusCode >= 300 && fetchResult.statusCode < 400 && fetchResult.redirectChain.length > 0) {
|
|
221
|
+
const lastHop = fetchResult.redirectChain[fetchResult.redirectChain.length - 1];
|
|
222
|
+
try {
|
|
223
|
+
if (new URL(lastHop.url).hostname === baseHost) {
|
|
224
|
+
redirectingInternalLinks.push({ source: fetchResult.url, target: fetchResult.finalUrl });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
163
230
|
return {
|
|
164
231
|
url: fetchResult.finalUrl,
|
|
165
232
|
depth,
|
|
166
233
|
statusCode: fetchResult.statusCode,
|
|
167
234
|
fetchResult,
|
|
168
235
|
pageData,
|
|
169
|
-
linksFound: internalLinks,
|
|
236
|
+
linksFound: [...extracted.internalLinks, ...extracted.paginationUrls, ...extracted.canonicalUrls],
|
|
170
237
|
crawlDuration: Date.now() - pageStart,
|
|
238
|
+
_extracted: extracted,
|
|
171
239
|
};
|
|
172
240
|
}
|
|
173
241
|
catch (err) {
|
|
@@ -194,6 +262,16 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
194
262
|
const identity = urlIdentity(page.url);
|
|
195
263
|
visitedIdentities.add(identity);
|
|
196
264
|
pages.push(page);
|
|
265
|
+
// Aggregate link counters from the extracted metadata.
|
|
266
|
+
const ext = page._extracted;
|
|
267
|
+
if (ext) {
|
|
268
|
+
nofollowLinks += ext.nofollowCount;
|
|
269
|
+
sponsoredLinks += ext.sponsoredCount;
|
|
270
|
+
ugcLinks += ext.ugcCount;
|
|
271
|
+
paginationLinks += ext.paginationUrls.length;
|
|
272
|
+
for (const c of ext.canonicalUrls)
|
|
273
|
+
canonicalUrlSet.add(c);
|
|
274
|
+
}
|
|
197
275
|
if (page.pageData) {
|
|
198
276
|
for (const link of page.pageData.links) {
|
|
199
277
|
if (link.type === "external")
|
|
@@ -255,6 +333,7 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
255
333
|
depth: p.depth,
|
|
256
334
|
statusCode: p.statusCode,
|
|
257
335
|
}));
|
|
336
|
+
const keywordCannibalization = detectKeywordCannibalization(pages);
|
|
258
337
|
return {
|
|
259
338
|
baseUrl: normalizedStart,
|
|
260
339
|
pagesCrawled: pages.length,
|
|
@@ -266,6 +345,13 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
266
345
|
pages,
|
|
267
346
|
linkGraph,
|
|
268
347
|
errors,
|
|
348
|
+
canonicalUrls: [...canonicalUrlSet],
|
|
349
|
+
nofollowLinks,
|
|
350
|
+
sponsoredLinks,
|
|
351
|
+
ugcLinks,
|
|
352
|
+
paginationLinks,
|
|
353
|
+
redirectingInternalLinks,
|
|
354
|
+
keywordCannibalization,
|
|
269
355
|
};
|
|
270
356
|
}
|
|
271
357
|
function detectDuplicates(pages) {
|
|
@@ -322,6 +408,46 @@ function detectDuplicates(pages) {
|
|
|
322
408
|
}
|
|
323
409
|
return groups;
|
|
324
410
|
}
|
|
411
|
+
function detectKeywordCannibalization(pages) {
|
|
412
|
+
const NGRAM_MIN = 3;
|
|
413
|
+
const wordRe = /\b[a-z]{3,}\b/g;
|
|
414
|
+
const phraseMap = new Map();
|
|
415
|
+
for (const page of pages) {
|
|
416
|
+
if (!page.pageData)
|
|
417
|
+
continue;
|
|
418
|
+
const title = page.pageData.title?.toLowerCase() ?? "";
|
|
419
|
+
const h1Texts = page.pageData.headings
|
|
420
|
+
.filter((h) => h.level === 1)
|
|
421
|
+
.map((h) => h.text.toLowerCase());
|
|
422
|
+
const combined = `${title} ${h1Texts.join(" ")}`;
|
|
423
|
+
const words = combined.match(wordRe);
|
|
424
|
+
if (!words || words.length < NGRAM_MIN)
|
|
425
|
+
continue;
|
|
426
|
+
for (let i = 0; i <= words.length - NGRAM_MIN; i++) {
|
|
427
|
+
const ngram = words.slice(i, i + NGRAM_MIN).join(" ");
|
|
428
|
+
if (!phraseMap.has(ngram))
|
|
429
|
+
phraseMap.set(ngram, []);
|
|
430
|
+
phraseMap.get(ngram).push(page.url);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const results = [];
|
|
434
|
+
const dedupe = new Map();
|
|
435
|
+
for (const [ngram, urls] of phraseMap) {
|
|
436
|
+
const uniquePages = [...new Set(urls)];
|
|
437
|
+
if (uniquePages.length >= 2) {
|
|
438
|
+
uniquePages.sort();
|
|
439
|
+
const sig = ngram + "|" + uniquePages.join("|");
|
|
440
|
+
if (!dedupe.has(sig)) {
|
|
441
|
+
dedupe.set(sig, uniquePages);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const [sig, urls] of dedupe) {
|
|
446
|
+
const keyword = sig.split("|")[0];
|
|
447
|
+
results.push({ keyword, pages: urls });
|
|
448
|
+
}
|
|
449
|
+
return results.sort((a, b) => b.pages.length - a.pages.length).slice(0, 20);
|
|
450
|
+
}
|
|
325
451
|
function sleep(ms) {
|
|
326
452
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
327
453
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/drift.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/drift.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AA4D9C,kEAAkE;AAClE,wBAAgB,YAAY,IAAI,IAAI,CASnC;AAMD,6CAA6C;AAC7C,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,aAAa,CAAC,CAmExB;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAWrG;AAyBD,qDAAqD;AACrD,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CAuG7B;AAED,sCAAsC;AACtC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,SAAK,GAAG,aAAa,EAAE,CAK9F"}
|