crawlemon 0.3.0 → 0.3.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 +9 -3
- package/dist/index.js +87 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@ Crawlemon is the SEO safety layer between code and production. It works like an
|
|
|
6
6
|
|
|
7
7
|
No account, cloud connection, browser, AI provider, or API key is required.
|
|
8
8
|
|
|
9
|
+
## What is new in 0.3.1
|
|
10
|
+
|
|
11
|
+
- Canonical URLs are no longer required on `robots: noindex` pages.
|
|
12
|
+
- `<h1>` duplicates in mutually exclusive early-return branches (auth gates, loading/success/error states) are no longer reported.
|
|
13
|
+
- Empty `alt` on dynamic-src decorative images (e.g. favicons) is no longer flagged.
|
|
14
|
+
|
|
9
15
|
## What is new in 0.3.0
|
|
10
16
|
|
|
11
17
|
- Production-grade static resolution for Next.js: site config, `metadataBase`, title objects (`default`/`template`/`absolute`), and layout inheritance.
|
|
@@ -217,11 +223,11 @@ jobs:
|
|
|
217
223
|
- name: Audit SEO
|
|
218
224
|
env:
|
|
219
225
|
CI: true
|
|
220
|
-
run: npx --yes crawlemon@0.3.
|
|
226
|
+
run: npx --yes crawlemon@0.3.1 audit
|
|
221
227
|
```
|
|
222
228
|
|
|
223
229
|
The exact package version is pinned so an upstream release cannot unexpectedly
|
|
224
|
-
change your CI result. Update `crawlemon@0.3.
|
|
230
|
+
change your CI result. Update `crawlemon@0.3.1` deliberately when you are ready
|
|
225
231
|
to adopt a newer version.
|
|
226
232
|
|
|
227
233
|
With `CI=true`, the workflow fails when Crawlemon finds at least one error-level
|
|
@@ -236,7 +242,7 @@ working directory:
|
|
|
236
242
|
working-directory: apps/web
|
|
237
243
|
env:
|
|
238
244
|
CI: true
|
|
239
|
-
run: npx --yes crawlemon@0.3.
|
|
245
|
+
run: npx --yes crawlemon@0.3.1 audit
|
|
240
246
|
```
|
|
241
247
|
|
|
242
248
|
## Zero-configuration detection
|
package/dist/index.js
CHANGED
|
@@ -1547,7 +1547,7 @@ function discoverImportedH1(filePath, resolver, maxDepth = 3) {
|
|
|
1547
1547
|
if (nextContent === void 0) continue;
|
|
1548
1548
|
const h1s = collectInlineHeadings(nextContent).filter((h) => h.level === 1);
|
|
1549
1549
|
if (h1s.length > 0) {
|
|
1550
|
-
result.push(
|
|
1550
|
+
result.push(h1s[0]);
|
|
1551
1551
|
continue;
|
|
1552
1552
|
}
|
|
1553
1553
|
for (const next of localComponentImports(nextContent, file, resolver)) {
|
|
@@ -1556,6 +1556,55 @@ function discoverImportedH1(filePath, resolver, maxDepth = 3) {
|
|
|
1556
1556
|
}
|
|
1557
1557
|
return result;
|
|
1558
1558
|
}
|
|
1559
|
+
function findDefaultExportBody(content) {
|
|
1560
|
+
const idx = content.search(/export\s+default\b/);
|
|
1561
|
+
if (idx === -1) return null;
|
|
1562
|
+
const head = content.slice(idx);
|
|
1563
|
+
if (/export\s+default\s+(?:async\s+)?function\b/.test(head)) {
|
|
1564
|
+
const openParen = head.indexOf("(", head.indexOf("function"));
|
|
1565
|
+
if (openParen === -1) return null;
|
|
1566
|
+
let cursor = readBalanced(head, openParen) + 1;
|
|
1567
|
+
while (cursor < head.length && head[cursor] !== "{") cursor += 1;
|
|
1568
|
+
if (head[cursor] !== "{") return null;
|
|
1569
|
+
const end = readBalanced(head, cursor);
|
|
1570
|
+
return { start: idx + cursor, end: idx + end };
|
|
1571
|
+
}
|
|
1572
|
+
const arrow = head.indexOf("=>");
|
|
1573
|
+
if (arrow !== -1) {
|
|
1574
|
+
let cursor = arrow + 2;
|
|
1575
|
+
while (cursor < head.length && /\s/.test(head[cursor])) cursor += 1;
|
|
1576
|
+
if (head[cursor] === "{") {
|
|
1577
|
+
const end = readBalanced(head, cursor);
|
|
1578
|
+
return { start: idx + cursor, end: idx + end };
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
function computeH1Concurrency(content, h1Lines) {
|
|
1584
|
+
const body = findDefaultExportBody(content);
|
|
1585
|
+
if (!body) return null;
|
|
1586
|
+
const { start, end } = body;
|
|
1587
|
+
const blockStarts = [];
|
|
1588
|
+
const returnRe = /(?:^|[^A-Za-z0-9_$])return\s*(?:\(\s*)?</g;
|
|
1589
|
+
for (const match of content.slice(start, end).matchAll(returnRe)) {
|
|
1590
|
+
const absolute = start + (match.index || 0);
|
|
1591
|
+
blockStarts.push(lineAt2(content, absolute));
|
|
1592
|
+
}
|
|
1593
|
+
if (blockStarts.length === 0) return null;
|
|
1594
|
+
blockStarts.sort((a, b) => a - b);
|
|
1595
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const h1Line of h1Lines) {
|
|
1597
|
+
let owner = blockStarts[0];
|
|
1598
|
+
for (const block of blockStarts) {
|
|
1599
|
+
if (block <= h1Line) owner = block;
|
|
1600
|
+
else break;
|
|
1601
|
+
}
|
|
1602
|
+
counts.set(owner, (counts.get(owner) || 0) + 1);
|
|
1603
|
+
}
|
|
1604
|
+
let max = 0;
|
|
1605
|
+
for (const count of counts.values()) max = Math.max(max, count);
|
|
1606
|
+
return max;
|
|
1607
|
+
}
|
|
1559
1608
|
|
|
1560
1609
|
// ../next-adapter/src/scanner.ts
|
|
1561
1610
|
function normalizeRouteGroup(segment) {
|
|
@@ -1698,6 +1747,14 @@ var NextJsAdapter = class {
|
|
|
1698
1747
|
headings.push(...discoverImportedH1(fullPath, resolver));
|
|
1699
1748
|
}
|
|
1700
1749
|
const dynamic = resolver ? dynamicParamsForFile(fullPath, resolver) : void 0;
|
|
1750
|
+
const inlineH1 = parsed.headings.filter((h) => h.level === 1);
|
|
1751
|
+
let maxConcurrentH1;
|
|
1752
|
+
if (inlineH1.length > 0) {
|
|
1753
|
+
const concurrency = computeH1Concurrency(content, inlineH1.map((h) => h.line || 0));
|
|
1754
|
+
maxConcurrentH1 = concurrency === null ? inlineH1.length : concurrency;
|
|
1755
|
+
} else {
|
|
1756
|
+
maxConcurrentH1 = headings.filter((h) => h.level === 1).length;
|
|
1757
|
+
}
|
|
1701
1758
|
routes.push({
|
|
1702
1759
|
route: routePath,
|
|
1703
1760
|
filePath: fullPath,
|
|
@@ -1710,7 +1767,8 @@ var NextJsAdapter = class {
|
|
|
1710
1767
|
hasDynamicSegments: routePath.includes("["),
|
|
1711
1768
|
dynamicParams: dynamic?.dynamicParams,
|
|
1712
1769
|
generatedParams: dynamic?.generatedParams,
|
|
1713
|
-
isRedirect: isRedirectOnly(content)
|
|
1770
|
+
isRedirect: isRedirectOnly(content),
|
|
1771
|
+
maxConcurrentH1
|
|
1714
1772
|
});
|
|
1715
1773
|
}
|
|
1716
1774
|
}
|
|
@@ -1738,6 +1796,9 @@ var NextJsAdapter = class {
|
|
|
1738
1796
|
if (routePath === "//" || routePath === "") routePath = "/";
|
|
1739
1797
|
const content = fs3.readFileSync(fullPath, "utf8");
|
|
1740
1798
|
const parsed = parsePageSource(content);
|
|
1799
|
+
const inlineH1 = parsed.headings.filter((h) => h.level === 1);
|
|
1800
|
+
const concurrency = computeH1Concurrency(content, inlineH1.map((h) => h.line || 0));
|
|
1801
|
+
const maxConcurrentH1 = concurrency === null ? inlineH1.length : concurrency;
|
|
1741
1802
|
routes.push({
|
|
1742
1803
|
route: routePath,
|
|
1743
1804
|
filePath: fullPath,
|
|
@@ -1747,7 +1808,8 @@ var NextJsAdapter = class {
|
|
|
1747
1808
|
links: parsed.links,
|
|
1748
1809
|
textContent: parsed.textContent,
|
|
1749
1810
|
hasLittleContent: parsed.hasLittleContent,
|
|
1750
|
-
hasDynamicSegments: routePath.includes("[")
|
|
1811
|
+
hasDynamicSegments: routePath.includes("["),
|
|
1812
|
+
maxConcurrentH1
|
|
1751
1813
|
});
|
|
1752
1814
|
}
|
|
1753
1815
|
}
|
|
@@ -2220,6 +2282,7 @@ var canonicalRule = {
|
|
|
2220
2282
|
}
|
|
2221
2283
|
for (const route of context.routes) {
|
|
2222
2284
|
if (route.isRedirect) continue;
|
|
2285
|
+
if ((route.metadata.robots || "").toLowerCase().includes("noindex")) continue;
|
|
2223
2286
|
if (route.metadata.dynamicMetadata) continue;
|
|
2224
2287
|
const canonical = route.metadata.canonical;
|
|
2225
2288
|
if (!canonical || canonical.trim() === "") {
|
|
@@ -2364,19 +2427,22 @@ var headingsRule = {
|
|
|
2364
2427
|
fixable: false,
|
|
2365
2428
|
explanation: "Every page should have exactly one <h1> element defining its primary topic for search engines and accessibility."
|
|
2366
2429
|
});
|
|
2367
|
-
} else
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2430
|
+
} else {
|
|
2431
|
+
const effectiveCount = route.maxConcurrentH1 !== void 0 ? Math.max(route.maxConcurrentH1, h1s.length > 0 ? 1 : 0) : h1s.length;
|
|
2432
|
+
if (effectiveCount > 1) {
|
|
2433
|
+
findings.push({
|
|
2434
|
+
id: `heading-multiple-h1-${route.route}`,
|
|
2435
|
+
rule: "headings",
|
|
2436
|
+
severity: "warning",
|
|
2437
|
+
category: "content",
|
|
2438
|
+
message: `Found ${effectiveCount} <h1> tags on route "${route.route}". Best practice is a single prominent <h1>.`,
|
|
2439
|
+
file: route.filePath,
|
|
2440
|
+
line: h1s[1].line,
|
|
2441
|
+
route: route.route,
|
|
2442
|
+
fixable: false,
|
|
2443
|
+
explanation: "Multiple <h1> tags can dilute topical hierarchy and confuse screen readers."
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2380
2446
|
}
|
|
2381
2447
|
let prevLevel = 1;
|
|
2382
2448
|
for (const h of headings) {
|
|
@@ -2438,7 +2504,9 @@ var imagesRule = {
|
|
|
2438
2504
|
// Never invent alt descriptions without AI
|
|
2439
2505
|
explanation: "Missing alt attributes harm accessibility and prevent images from ranking in Google Image Search."
|
|
2440
2506
|
});
|
|
2441
|
-
} else if (img.alt.trim() === "" && !img.src.includes("icon") && !img.src.includes("decorative"))
|
|
2507
|
+
} else if (img.alt.trim() === "" && !img.src.includes("icon") && !img.src.includes("decorative") && // A runtime/dynamic src (e.g. favicon from data) cannot be judged;
|
|
2508
|
+
// empty alt for it is commonly the intended decorative usage.
|
|
2509
|
+
!img.src.includes("unknown-image")) {
|
|
2442
2510
|
findings.push({
|
|
2443
2511
|
id: `img-empty-alt-${route.route}-${img.src}`,
|
|
2444
2512
|
rule: "images",
|
|
@@ -3803,7 +3871,7 @@ function runSEOAudit(options) {
|
|
|
3803
3871
|
recommendations,
|
|
3804
3872
|
opportunities,
|
|
3805
3873
|
timestamp,
|
|
3806
|
-
engineVersion: "0.3.
|
|
3874
|
+
engineVersion: "0.3.1"
|
|
3807
3875
|
};
|
|
3808
3876
|
}
|
|
3809
3877
|
|
|
@@ -5721,7 +5789,7 @@ async function runCli(args) {
|
|
|
5721
5789
|
return;
|
|
5722
5790
|
}
|
|
5723
5791
|
if (command === "--version" || command === "-v") {
|
|
5724
|
-
console.log("0.3.
|
|
5792
|
+
console.log("0.3.1");
|
|
5725
5793
|
return;
|
|
5726
5794
|
}
|
|
5727
5795
|
checkSecretRisk(projectRoot);
|