@houseofmvps/claude-rank 1.0.2 → 1.1.0
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/package.json +1 -1
- package/tools/aeo-scanner.mjs +14 -1
- package/tools/geo-scanner.mjs +32 -9
- package/tools/lib/html-parser.mjs +28 -2
- package/tools/seo-scanner.mjs +13 -1
package/package.json
CHANGED
package/tools/aeo-scanner.mjs
CHANGED
|
@@ -208,7 +208,20 @@ function analyzePage(filePath) {
|
|
|
208
208
|
* @returns {{ files_scanned, findings, scores: { aeo }, summary }}
|
|
209
209
|
*/
|
|
210
210
|
export function scanDirectory(rootDir) {
|
|
211
|
-
|
|
211
|
+
let htmlFiles = findHtmlFiles(rootDir);
|
|
212
|
+
|
|
213
|
+
// If dist/build/out has HTML, exclude root index.html (Vite/webpack source template)
|
|
214
|
+
const hasBuildDir = htmlFiles.some(f => {
|
|
215
|
+
const rel = path.relative(rootDir, f);
|
|
216
|
+
return rel.startsWith('dist' + path.sep) || rel.startsWith('build' + path.sep) || rel.startsWith('out' + path.sep);
|
|
217
|
+
});
|
|
218
|
+
if (hasBuildDir) {
|
|
219
|
+
htmlFiles = htmlFiles.filter(f => {
|
|
220
|
+
const rel = path.relative(rootDir, f);
|
|
221
|
+
return rel !== 'index.html' && rel !== 'index.htm';
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
212
225
|
const findings = [];
|
|
213
226
|
|
|
214
227
|
// Per-file analyses
|
package/tools/geo-scanner.mjs
CHANGED
|
@@ -155,16 +155,26 @@ function parseRobotsTxt(content) {
|
|
|
155
155
|
*/
|
|
156
156
|
function extractSchemaTypes(jsonLdContent) {
|
|
157
157
|
const types = new Set();
|
|
158
|
+
|
|
159
|
+
function walkSchema(obj) {
|
|
160
|
+
if (!obj || typeof obj !== 'object') return;
|
|
161
|
+
if (Array.isArray(obj)) {
|
|
162
|
+
for (const item of obj) walkSchema(item);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (obj['@type']) {
|
|
166
|
+
const t = Array.isArray(obj['@type']) ? obj['@type'] : [obj['@type']];
|
|
167
|
+
for (const type of t) types.add(type);
|
|
168
|
+
}
|
|
169
|
+
// Walk all nested objects to find embedded schemas (e.g., author: { @type: "Person" })
|
|
170
|
+
for (const val of Object.values(obj)) {
|
|
171
|
+
if (val && typeof val === 'object') walkSchema(val);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
for (const raw of jsonLdContent) {
|
|
159
176
|
try {
|
|
160
|
-
|
|
161
|
-
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
162
|
-
for (const item of items) {
|
|
163
|
-
if (item && item['@type']) {
|
|
164
|
-
const t = Array.isArray(item['@type']) ? item['@type'] : [item['@type']];
|
|
165
|
-
for (const type of t) types.add(type);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
177
|
+
walkSchema(JSON.parse(raw));
|
|
168
178
|
} catch {
|
|
169
179
|
// Non-parseable JSON-LD — skip
|
|
170
180
|
}
|
|
@@ -307,7 +317,20 @@ export function scanDirectory(rootDir) {
|
|
|
307
317
|
// 3. Scan HTML files
|
|
308
318
|
// -------------------------------------------------------------------------
|
|
309
319
|
|
|
310
|
-
|
|
320
|
+
let htmlFiles = findHtmlFiles(rootDir);
|
|
321
|
+
|
|
322
|
+
// If dist/build/out has HTML, exclude root index.html (Vite/webpack source template)
|
|
323
|
+
const hasBuildDir = htmlFiles.some(f => {
|
|
324
|
+
const rel = path.relative(rootDir, f);
|
|
325
|
+
return rel.startsWith('dist' + path.sep) || rel.startsWith('build' + path.sep) || rel.startsWith('out' + path.sep);
|
|
326
|
+
});
|
|
327
|
+
if (hasBuildDir) {
|
|
328
|
+
htmlFiles = htmlFiles.filter(f => {
|
|
329
|
+
const rel = path.relative(rootDir, f);
|
|
330
|
+
return rel !== 'index.html' && rel !== 'index.htm';
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
311
334
|
let filesScanned = 0;
|
|
312
335
|
|
|
313
336
|
// Aggregate data across all pages
|
|
@@ -127,6 +127,8 @@ export function parseHtml(htmlString) {
|
|
|
127
127
|
let currentHeadingLevel = 0;
|
|
128
128
|
let isJsonLd = false;
|
|
129
129
|
let currentHeadingText = '';
|
|
130
|
+
let currentScriptSrc = '';
|
|
131
|
+
let inlineScriptBuffer = '';
|
|
130
132
|
let bodyTextBuffer = '';
|
|
131
133
|
|
|
132
134
|
const parser = new Parser(
|
|
@@ -252,8 +254,9 @@ export function parseHtml(htmlString) {
|
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
// Count total and deferred scripts
|
|
257
|
+
// type="module" is deferred by default per HTML spec
|
|
255
258
|
state.totalScripts++;
|
|
256
|
-
if (attribs.async !== undefined || attribs.defer !== undefined) {
|
|
259
|
+
if (attribs.async !== undefined || attribs.defer !== undefined || scriptType === 'module') {
|
|
257
260
|
state.deferredScripts++;
|
|
258
261
|
}
|
|
259
262
|
|
|
@@ -269,6 +272,7 @@ export function parseHtml(htmlString) {
|
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
inScript = true;
|
|
275
|
+
currentScriptSrc = src;
|
|
272
276
|
return;
|
|
273
277
|
}
|
|
274
278
|
|
|
@@ -349,6 +353,12 @@ export function parseHtml(htmlString) {
|
|
|
349
353
|
return;
|
|
350
354
|
}
|
|
351
355
|
|
|
356
|
+
// Inline script content — accumulate for analytics detection
|
|
357
|
+
if (inScript && !isJsonLd) {
|
|
358
|
+
inlineScriptBuffer += text;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
352
362
|
// Body text (skip script/style)
|
|
353
363
|
if (inBody && !inScript && !inStyle) {
|
|
354
364
|
bodyTextBuffer += text + ' ';
|
|
@@ -372,7 +382,19 @@ export function parseHtml(htmlString) {
|
|
|
372
382
|
state.jsonLdScripts++;
|
|
373
383
|
isJsonLd = false;
|
|
374
384
|
}
|
|
385
|
+
// Check inline script content for analytics patterns (catches lazy-loaded GA etc.)
|
|
386
|
+
if (!state.hasAnalytics && !currentScriptSrc && inlineScriptBuffer) {
|
|
387
|
+
for (const { pattern, provider } of ANALYTICS_PATTERNS) {
|
|
388
|
+
if (inlineScriptBuffer.includes(pattern)) {
|
|
389
|
+
state.hasAnalytics = true;
|
|
390
|
+
state.analyticsProvider = provider;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
375
395
|
inScript = false;
|
|
396
|
+
currentScriptSrc = '';
|
|
397
|
+
inlineScriptBuffer = '';
|
|
376
398
|
return;
|
|
377
399
|
}
|
|
378
400
|
|
|
@@ -451,7 +473,9 @@ export async function parseHtmlFile(filePath) {
|
|
|
451
473
|
// findHtmlFiles — recursively find .html/.htm files
|
|
452
474
|
// ---------------------------------------------------------------------------
|
|
453
475
|
|
|
454
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.cache', '.turbo']);
|
|
476
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.cache', '.turbo', 'public']);
|
|
477
|
+
// Files that look like HTML but aren't real pages (e.g., Google/Bing site verification)
|
|
478
|
+
const SKIP_FILE_PATTERNS = [/^google[a-f0-9]+\.html$/, /^bing[a-f0-9]+\.html$/, /^yandex_[a-f0-9]+\.html$/];
|
|
455
479
|
|
|
456
480
|
/**
|
|
457
481
|
* Recursively find all .html/.htm files under a directory.
|
|
@@ -479,6 +503,8 @@ export function findHtmlFiles(dir) {
|
|
|
479
503
|
} else if (entry.isFile()) {
|
|
480
504
|
const ext = path.extname(entry.name).toLowerCase();
|
|
481
505
|
if (ext === '.html' || ext === '.htm') {
|
|
506
|
+
// Skip search engine verification files
|
|
507
|
+
if (SKIP_FILE_PATTERNS.some(p => p.test(entry.name))) continue;
|
|
482
508
|
results.push(fullPath);
|
|
483
509
|
}
|
|
484
510
|
}
|
package/tools/seo-scanner.mjs
CHANGED
|
@@ -436,7 +436,19 @@ function calculateScore(findings) {
|
|
|
436
436
|
*/
|
|
437
437
|
export function scanDirectory(rootDir) {
|
|
438
438
|
const absRoot = path.resolve(rootDir);
|
|
439
|
-
|
|
439
|
+
let htmlFiles = findHtmlFiles(absRoot);
|
|
440
|
+
|
|
441
|
+
// If dist/ or build/ has HTML, exclude root index.html (Vite/webpack source template)
|
|
442
|
+
const hasBuildDir = htmlFiles.some(f => {
|
|
443
|
+
const rel = path.relative(absRoot, f);
|
|
444
|
+
return rel.startsWith('dist' + path.sep) || rel.startsWith('build' + path.sep) || rel.startsWith('out' + path.sep);
|
|
445
|
+
});
|
|
446
|
+
if (hasBuildDir) {
|
|
447
|
+
htmlFiles = htmlFiles.filter(f => {
|
|
448
|
+
const rel = path.relative(absRoot, f);
|
|
449
|
+
return rel !== 'index.html' && rel !== 'index.htm';
|
|
450
|
+
});
|
|
451
|
+
}
|
|
440
452
|
|
|
441
453
|
// Backend-only detection
|
|
442
454
|
if (isBackendOnlyProject(absRoot, htmlFiles)) {
|