@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houseofmvps/claude-rank",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "The most comprehensive SEO/GEO/AEO plugin for Claude Code. Audit, fix, and dominate search — traditional and AI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- const htmlFiles = findHtmlFiles(rootDir);
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
@@ -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
- const parsed = JSON.parse(raw);
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
- const htmlFiles = findHtmlFiles(rootDir);
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
  }
@@ -436,7 +436,19 @@ function calculateScore(findings) {
436
436
  */
437
437
  export function scanDirectory(rootDir) {
438
438
  const absRoot = path.resolve(rootDir);
439
- const htmlFiles = findHtmlFiles(absRoot);
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)) {