recker 1.0.26 → 1.0.27-next.1cb21f8
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/browser/browser/cache.d.ts +40 -0
- package/dist/browser/browser/cache.js +199 -0
- package/dist/browser/browser/crypto.d.ts +24 -0
- package/dist/browser/browser/crypto.js +80 -0
- package/dist/browser/browser/index.d.ts +31 -0
- package/dist/browser/browser/index.js +31 -0
- package/dist/browser/browser/recker.d.ts +26 -0
- package/dist/browser/browser/recker.js +61 -0
- package/dist/browser/cache/basic-file-storage.d.ts +12 -0
- package/dist/browser/cache/basic-file-storage.js +50 -0
- package/dist/browser/cache/memory-limits.d.ts +20 -0
- package/dist/browser/cache/memory-limits.js +96 -0
- package/dist/browser/cache/memory-storage.d.ts +132 -0
- package/dist/browser/cache/memory-storage.js +454 -0
- package/dist/browser/cache.d.ts +40 -0
- package/dist/browser/cache.js +199 -0
- package/dist/browser/constants/http-status.d.ts +73 -0
- package/dist/browser/constants/http-status.js +156 -0
- package/dist/browser/cookies/memory-cookie-jar.d.ts +30 -0
- package/dist/browser/cookies/memory-cookie-jar.js +210 -0
- package/dist/browser/core/client.d.ts +118 -0
- package/dist/browser/core/client.js +667 -0
- package/dist/browser/core/errors.d.ts +142 -0
- package/dist/browser/core/errors.js +308 -0
- package/dist/browser/core/index.d.ts +5 -0
- package/dist/browser/core/index.js +5 -0
- package/dist/browser/core/request-promise.d.ts +23 -0
- package/dist/browser/core/request-promise.js +82 -0
- package/dist/browser/core/request.d.ts +20 -0
- package/dist/browser/core/request.js +76 -0
- package/dist/browser/core/response.d.ts +34 -0
- package/dist/browser/core/response.js +178 -0
- package/dist/browser/crypto.d.ts +24 -0
- package/dist/browser/crypto.js +80 -0
- package/dist/browser/index.d.ts +31 -0
- package/dist/browser/index.js +31 -0
- package/dist/browser/plugins/auth/api-key.d.ts +8 -0
- package/dist/browser/plugins/auth/api-key.js +27 -0
- package/dist/browser/plugins/auth/auth0.d.ts +33 -0
- package/dist/browser/plugins/auth/auth0.js +94 -0
- package/dist/browser/plugins/auth/aws-sigv4.d.ts +10 -0
- package/dist/browser/plugins/auth/aws-sigv4.js +88 -0
- package/dist/browser/plugins/auth/azure-ad.d.ts +48 -0
- package/dist/browser/plugins/auth/azure-ad.js +152 -0
- package/dist/browser/plugins/auth/basic.d.ts +7 -0
- package/dist/browser/plugins/auth/basic.js +13 -0
- package/dist/browser/plugins/auth/bearer.d.ts +8 -0
- package/dist/browser/plugins/auth/bearer.js +17 -0
- package/dist/browser/plugins/auth/cognito.d.ts +45 -0
- package/dist/browser/plugins/auth/cognito.js +208 -0
- package/dist/browser/plugins/auth/digest.d.ts +8 -0
- package/dist/browser/plugins/auth/digest.js +100 -0
- package/dist/browser/plugins/auth/firebase.d.ts +32 -0
- package/dist/browser/plugins/auth/firebase.js +195 -0
- package/dist/browser/plugins/auth/github-app.d.ts +36 -0
- package/dist/browser/plugins/auth/github-app.js +170 -0
- package/dist/browser/plugins/auth/google-service-account.d.ts +49 -0
- package/dist/browser/plugins/auth/google-service-account.js +172 -0
- package/dist/browser/plugins/auth/index.d.ts +15 -0
- package/dist/browser/plugins/auth/index.js +15 -0
- package/dist/browser/plugins/auth/mtls.d.ts +37 -0
- package/dist/browser/plugins/auth/mtls.js +140 -0
- package/dist/browser/plugins/auth/oauth2.d.ts +8 -0
- package/dist/browser/plugins/auth/oauth2.js +26 -0
- package/dist/browser/plugins/auth/oidc.d.ts +55 -0
- package/dist/browser/plugins/auth/oidc.js +222 -0
- package/dist/browser/plugins/auth/okta.d.ts +47 -0
- package/dist/browser/plugins/auth/okta.js +157 -0
- package/dist/browser/plugins/auth.d.ts +1 -0
- package/dist/browser/plugins/auth.js +1 -0
- package/dist/browser/plugins/cache.d.ts +15 -0
- package/dist/browser/plugins/cache.js +486 -0
- package/dist/browser/plugins/circuit-breaker.d.ts +13 -0
- package/dist/browser/plugins/circuit-breaker.js +100 -0
- package/dist/browser/plugins/compression.d.ts +4 -0
- package/dist/browser/plugins/compression.js +130 -0
- package/dist/browser/plugins/cookie-jar.d.ts +5 -0
- package/dist/browser/plugins/cookie-jar.js +72 -0
- package/dist/browser/plugins/dedup.d.ts +5 -0
- package/dist/browser/plugins/dedup.js +35 -0
- package/dist/browser/plugins/graphql.d.ts +13 -0
- package/dist/browser/plugins/graphql.js +58 -0
- package/dist/browser/plugins/grpc-web.d.ts +79 -0
- package/dist/browser/plugins/grpc-web.js +261 -0
- package/dist/browser/plugins/hls.d.ts +105 -0
- package/dist/browser/plugins/hls.js +395 -0
- package/dist/browser/plugins/jsonrpc.d.ts +75 -0
- package/dist/browser/plugins/jsonrpc.js +143 -0
- package/dist/browser/plugins/logger.d.ts +13 -0
- package/dist/browser/plugins/logger.js +108 -0
- package/dist/browser/plugins/odata.d.ts +181 -0
- package/dist/browser/plugins/odata.js +564 -0
- package/dist/browser/plugins/pagination.d.ts +16 -0
- package/dist/browser/plugins/pagination.js +105 -0
- package/dist/browser/plugins/rate-limit.d.ts +15 -0
- package/dist/browser/plugins/rate-limit.js +162 -0
- package/dist/browser/plugins/retry.d.ts +14 -0
- package/dist/browser/plugins/retry.js +116 -0
- package/dist/browser/plugins/scrape.d.ts +21 -0
- package/dist/browser/plugins/scrape.js +82 -0
- package/dist/browser/plugins/server-timing.d.ts +7 -0
- package/dist/browser/plugins/server-timing.js +24 -0
- package/dist/browser/plugins/soap.d.ts +72 -0
- package/dist/browser/plugins/soap.js +347 -0
- package/dist/browser/plugins/xml.d.ts +9 -0
- package/dist/browser/plugins/xml.js +194 -0
- package/dist/browser/plugins/xsrf.d.ts +9 -0
- package/dist/browser/plugins/xsrf.js +48 -0
- package/dist/browser/recker.d.ts +26 -0
- package/dist/browser/recker.js +61 -0
- package/dist/browser/runner/request-runner.d.ts +46 -0
- package/dist/browser/runner/request-runner.js +89 -0
- package/dist/browser/scrape/document.d.ts +44 -0
- package/dist/browser/scrape/document.js +210 -0
- package/dist/browser/scrape/element.d.ts +49 -0
- package/dist/browser/scrape/element.js +176 -0
- package/dist/browser/scrape/extractors.d.ts +16 -0
- package/dist/browser/scrape/extractors.js +357 -0
- package/dist/browser/scrape/types.d.ts +108 -0
- package/dist/browser/scrape/types.js +1 -0
- package/dist/browser/transport/fetch.d.ts +11 -0
- package/dist/browser/transport/fetch.js +143 -0
- package/dist/browser/transport/undici.d.ts +38 -0
- package/dist/browser/transport/undici.js +897 -0
- package/dist/browser/types/ai.d.ts +267 -0
- package/dist/browser/types/ai.js +1 -0
- package/dist/browser/types/index.d.ts +351 -0
- package/dist/browser/types/index.js +1 -0
- package/dist/browser/types/logger.d.ts +16 -0
- package/dist/browser/types/logger.js +66 -0
- package/dist/browser/types/udp.d.ts +138 -0
- package/dist/browser/types/udp.js +1 -0
- package/dist/browser/utils/agent-manager.d.ts +29 -0
- package/dist/browser/utils/agent-manager.js +160 -0
- package/dist/browser/utils/body.d.ts +10 -0
- package/dist/browser/utils/body.js +148 -0
- package/dist/browser/utils/charset.d.ts +15 -0
- package/dist/browser/utils/charset.js +169 -0
- package/dist/browser/utils/concurrency.d.ts +20 -0
- package/dist/browser/utils/concurrency.js +120 -0
- package/dist/browser/utils/dns.d.ts +6 -0
- package/dist/browser/utils/dns.js +26 -0
- package/dist/browser/utils/header-parser.d.ts +94 -0
- package/dist/browser/utils/header-parser.js +617 -0
- package/dist/browser/utils/html-cleaner.d.ts +1 -0
- package/dist/browser/utils/html-cleaner.js +21 -0
- package/dist/browser/utils/link-header.d.ts +69 -0
- package/dist/browser/utils/link-header.js +190 -0
- package/dist/browser/utils/optional-require.d.ts +19 -0
- package/dist/browser/utils/optional-require.js +105 -0
- package/dist/browser/utils/progress.d.ts +8 -0
- package/dist/browser/utils/progress.js +82 -0
- package/dist/browser/utils/request-pool.d.ts +22 -0
- package/dist/browser/utils/request-pool.js +101 -0
- package/dist/browser/utils/sse.d.ts +7 -0
- package/dist/browser/utils/sse.js +67 -0
- package/dist/browser/utils/streaming.d.ts +17 -0
- package/dist/browser/utils/streaming.js +84 -0
- package/dist/browser/utils/try-fn.d.ts +3 -0
- package/dist/browser/utils/try-fn.js +59 -0
- package/dist/browser/utils/user-agent.d.ts +44 -0
- package/dist/browser/utils/user-agent.js +100 -0
- package/dist/browser/utils/whois.d.ts +32 -0
- package/dist/browser/utils/whois.js +246 -0
- package/dist/browser/websocket/client.d.ts +65 -0
- package/dist/browser/websocket/client.js +313 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +143 -3
- package/dist/cli/tui/shell.d.ts +1 -0
- package/dist/cli/tui/shell.js +157 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scrape/extractors.js +2 -1
- package/dist/scrape/types.d.ts +2 -1
- package/dist/seo/analyzer.d.ts +42 -0
- package/dist/seo/analyzer.js +715 -0
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.js +2 -0
- package/dist/seo/rules/accessibility.d.ts +2 -0
- package/dist/seo/rules/accessibility.js +128 -0
- package/dist/seo/rules/content.d.ts +2 -0
- package/dist/seo/rules/content.js +236 -0
- package/dist/seo/rules/images.d.ts +2 -0
- package/dist/seo/rules/images.js +180 -0
- package/dist/seo/rules/index.d.ts +20 -0
- package/dist/seo/rules/index.js +72 -0
- package/dist/seo/rules/links.d.ts +2 -0
- package/dist/seo/rules/links.js +150 -0
- package/dist/seo/rules/meta.d.ts +2 -0
- package/dist/seo/rules/meta.js +523 -0
- package/dist/seo/rules/mobile.d.ts +2 -0
- package/dist/seo/rules/mobile.js +71 -0
- package/dist/seo/rules/performance.d.ts +2 -0
- package/dist/seo/rules/performance.js +246 -0
- package/dist/seo/rules/schema.d.ts +2 -0
- package/dist/seo/rules/schema.js +54 -0
- package/dist/seo/rules/security.d.ts +2 -0
- package/dist/seo/rules/security.js +147 -0
- package/dist/seo/rules/structural.d.ts +2 -0
- package/dist/seo/rules/structural.js +155 -0
- package/dist/seo/rules/technical.d.ts +2 -0
- package/dist/seo/rules/technical.js +223 -0
- package/dist/seo/rules/thresholds.d.ts +196 -0
- package/dist/seo/rules/thresholds.js +118 -0
- package/dist/seo/rules/types.d.ts +191 -0
- package/dist/seo/rules/types.js +11 -0
- package/dist/seo/types.d.ts +160 -0
- package/dist/seo/types.js +1 -0
- package/dist/transport/fetch.d.ts +7 -1
- package/dist/transport/fetch.js +58 -76
- package/dist/utils/columns.d.ts +14 -0
- package/dist/utils/columns.js +69 -0
- package/package.json +34 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
import { SEO_THRESHOLDS } from './thresholds.js';
|
|
3
|
+
export const performanceRules = [
|
|
4
|
+
{
|
|
5
|
+
id: 'perf-preconnect',
|
|
6
|
+
name: 'Preconnect Hints',
|
|
7
|
+
category: 'performance',
|
|
8
|
+
severity: 'info',
|
|
9
|
+
description: 'Use preconnect for important third-party origins',
|
|
10
|
+
check: (ctx) => {
|
|
11
|
+
if (!ctx.hasPreconnect && ctx.externalLinks && ctx.externalLinks > 5) {
|
|
12
|
+
return createResult({ id: 'perf-preconnect', name: 'Preconnect Hints', category: 'performance', severity: 'info' }, 'info', 'No preconnect hints found', { recommendation: 'Add <link rel="preconnect" href="..."> for important third-party domains' });
|
|
13
|
+
}
|
|
14
|
+
if (ctx.preconnectCount && ctx.preconnectCount > 0) {
|
|
15
|
+
return createResult({ id: 'perf-preconnect', name: 'Preconnect Hints', category: 'performance', severity: 'info' }, 'pass', `${ctx.preconnectCount} preconnect hint(s) found`, { value: ctx.preconnectCount });
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'perf-dns-prefetch',
|
|
22
|
+
name: 'DNS Prefetch',
|
|
23
|
+
category: 'performance',
|
|
24
|
+
severity: 'info',
|
|
25
|
+
description: 'Use dns-prefetch for external domains',
|
|
26
|
+
check: (ctx) => {
|
|
27
|
+
if (ctx.dnsPrefetchCount && ctx.dnsPrefetchCount > 0) {
|
|
28
|
+
return createResult({ id: 'perf-dns-prefetch', name: 'DNS Prefetch', category: 'performance', severity: 'info' }, 'pass', `${ctx.dnsPrefetchCount} dns-prefetch hint(s) found`, { value: ctx.dnsPrefetchCount });
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'perf-preload',
|
|
35
|
+
name: 'Preload Hints',
|
|
36
|
+
category: 'performance',
|
|
37
|
+
severity: 'info',
|
|
38
|
+
description: 'Use preload for critical resources',
|
|
39
|
+
check: (ctx) => {
|
|
40
|
+
if (ctx.preloadCount && ctx.preloadCount > 0) {
|
|
41
|
+
return createResult({ id: 'perf-preload', name: 'Preload Hints', category: 'performance', severity: 'info' }, 'pass', `${ctx.preloadCount} preload hint(s) found`, { value: ctx.preloadCount });
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'perf-render-blocking',
|
|
48
|
+
name: 'Render Blocking',
|
|
49
|
+
category: 'performance',
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
description: 'Minimize render-blocking resources',
|
|
52
|
+
check: (ctx) => {
|
|
53
|
+
const blocking = ctx.renderBlockingResources ?? 0;
|
|
54
|
+
if (blocking > 5) {
|
|
55
|
+
return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'warn', `${blocking} render-blocking resources in <head>`, { value: blocking, recommendation: 'Use async/defer for scripts, preload for critical CSS' });
|
|
56
|
+
}
|
|
57
|
+
if (blocking > 0) {
|
|
58
|
+
return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'info', `${blocking} render-blocking resource(s) in <head>`, { value: blocking });
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'perf-inline-styles',
|
|
65
|
+
name: 'Inline Styles',
|
|
66
|
+
category: 'performance',
|
|
67
|
+
severity: 'info',
|
|
68
|
+
description: 'Excessive inline styles can increase page size',
|
|
69
|
+
check: (ctx) => {
|
|
70
|
+
const inline = ctx.inlineStylesCount ?? 0;
|
|
71
|
+
if (inline > 10) {
|
|
72
|
+
return createResult({ id: 'perf-inline-styles', name: 'Inline Styles', category: 'performance', severity: 'info' }, 'info', `${inline} inline style blocks found`, { value: inline, recommendation: 'Consider consolidating inline styles into external CSS' });
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'cwv-lcp-lazy',
|
|
79
|
+
name: 'LCP Image Lazy',
|
|
80
|
+
category: 'performance',
|
|
81
|
+
severity: 'warning',
|
|
82
|
+
description: 'Above-the-fold images should not use lazy loading',
|
|
83
|
+
check: (ctx) => {
|
|
84
|
+
if (ctx.lcpHints?.hasLazyLcp) {
|
|
85
|
+
return createResult({ id: 'cwv-lcp-lazy', name: 'LCP Image Lazy', category: 'performance', severity: 'warning' }, 'warn', 'First large image uses lazy loading (may hurt LCP)', { recommendation: 'Remove loading="lazy" from above-the-fold images' });
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'cwv-lcp-priority',
|
|
92
|
+
name: 'LCP Priority Hints',
|
|
93
|
+
category: 'performance',
|
|
94
|
+
severity: 'info',
|
|
95
|
+
description: 'Use fetchpriority="high" for LCP images',
|
|
96
|
+
check: (ctx) => {
|
|
97
|
+
if (ctx.lcpHints?.hasLargeImages && !ctx.lcpHints?.hasPriorityHints) {
|
|
98
|
+
return createResult({ id: 'cwv-lcp-priority', name: 'LCP Priority Hints', category: 'performance', severity: 'info' }, 'info', 'No fetchpriority="high" found on large images', { recommendation: 'Add fetchpriority="high" to LCP candidate images' });
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'cwv-cls-images',
|
|
105
|
+
name: 'CLS Image Dimensions',
|
|
106
|
+
category: 'performance',
|
|
107
|
+
severity: 'warning',
|
|
108
|
+
description: 'Images without dimensions cause layout shifts',
|
|
109
|
+
check: (ctx) => {
|
|
110
|
+
const missing = ctx.clsHints?.imagesWithoutDimensions ?? ctx.imagesMissingDimensions ?? 0;
|
|
111
|
+
if (missing > 0) {
|
|
112
|
+
return createResult({ id: 'cwv-cls-images', name: 'CLS Image Dimensions', category: 'performance', severity: 'warning' }, 'warn', `${missing} image(s) without width/height (causes CLS)`, { value: missing, recommendation: 'Add width and height attributes to all images' });
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'timing-ttfb',
|
|
119
|
+
name: 'Time to First Byte',
|
|
120
|
+
category: 'performance',
|
|
121
|
+
severity: 'error',
|
|
122
|
+
description: 'TTFB should be under 600ms (ideally under 200ms)',
|
|
123
|
+
check: (ctx) => {
|
|
124
|
+
const ttfb = ctx.timings?.ttfb;
|
|
125
|
+
if (ttfb === undefined)
|
|
126
|
+
return null;
|
|
127
|
+
const { good, needsImprovement, poor } = SEO_THRESHOLDS.timing.ttfb;
|
|
128
|
+
if (ttfb <= good) {
|
|
129
|
+
return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'pass', `Excellent TTFB (${ttfb}ms)`, { value: ttfb });
|
|
130
|
+
}
|
|
131
|
+
if (ttfb <= needsImprovement) {
|
|
132
|
+
return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'info', `TTFB needs improvement (${ttfb}ms)`, { value: ttfb, recommendation: `Optimize server response time to under ${good}ms` });
|
|
133
|
+
}
|
|
134
|
+
if (ttfb <= poor) {
|
|
135
|
+
return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'warn', `Slow TTFB (${ttfb}ms)`, { value: ttfb, recommendation: 'Optimize server, use CDN, enable caching' });
|
|
136
|
+
}
|
|
137
|
+
return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'fail', `Very slow TTFB (${ttfb}ms)`, { value: ttfb, recommendation: 'Critical: Server is too slow. Check server, database, and network' });
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'timing-total',
|
|
142
|
+
name: 'Total Load Time',
|
|
143
|
+
category: 'performance',
|
|
144
|
+
severity: 'warning',
|
|
145
|
+
description: 'Total page load should be under 2.5s',
|
|
146
|
+
check: (ctx) => {
|
|
147
|
+
const total = ctx.timings?.total;
|
|
148
|
+
if (total === undefined)
|
|
149
|
+
return null;
|
|
150
|
+
const { good, needsImprovement, poor } = SEO_THRESHOLDS.timing.total;
|
|
151
|
+
if (total <= good) {
|
|
152
|
+
return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'pass', `Fast page load (${total}ms)`, { value: total });
|
|
153
|
+
}
|
|
154
|
+
if (total <= needsImprovement) {
|
|
155
|
+
return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'info', `Page load time acceptable (${total}ms)`, { value: total, recommendation: `Aim for under ${good}ms for better user experience` });
|
|
156
|
+
}
|
|
157
|
+
if (total <= poor) {
|
|
158
|
+
return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'warn', `Slow page load (${total}ms)`, { value: total, recommendation: 'Optimize assets, enable compression, use CDN' });
|
|
159
|
+
}
|
|
160
|
+
return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'fail', `Very slow page load (${total}ms)`, { value: total, recommendation: 'Critical performance issue. Full optimization needed.' });
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'timing-dns',
|
|
165
|
+
name: 'DNS Lookup',
|
|
166
|
+
category: 'performance',
|
|
167
|
+
severity: 'info',
|
|
168
|
+
description: 'DNS lookup should be under 50ms',
|
|
169
|
+
check: (ctx) => {
|
|
170
|
+
const dns = ctx.timings?.dnsLookup;
|
|
171
|
+
if (dns === undefined)
|
|
172
|
+
return null;
|
|
173
|
+
const { good, poor } = SEO_THRESHOLDS.timing.dnsLookup;
|
|
174
|
+
if (dns <= good) {
|
|
175
|
+
return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'pass', `Fast DNS lookup (${dns}ms)`, { value: dns });
|
|
176
|
+
}
|
|
177
|
+
if (dns <= poor) {
|
|
178
|
+
return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'info', `DNS lookup could be faster (${dns}ms)`, { value: dns, recommendation: 'Consider using faster DNS provider or dns-prefetch' });
|
|
179
|
+
}
|
|
180
|
+
return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'warn', `Slow DNS lookup (${dns}ms)`, { value: dns, recommendation: 'DNS is slow. Consider Cloudflare, Google DNS, or dns-prefetch' });
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'timing-tls',
|
|
185
|
+
name: 'TLS Handshake',
|
|
186
|
+
category: 'performance',
|
|
187
|
+
severity: 'info',
|
|
188
|
+
description: 'TLS handshake should be under 100ms',
|
|
189
|
+
check: (ctx) => {
|
|
190
|
+
const tls = ctx.timings?.tlsHandshake;
|
|
191
|
+
if (tls === undefined)
|
|
192
|
+
return null;
|
|
193
|
+
const { good, poor } = SEO_THRESHOLDS.timing.tlsHandshake;
|
|
194
|
+
if (tls <= good) {
|
|
195
|
+
return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'pass', `Fast TLS handshake (${tls}ms)`, { value: tls });
|
|
196
|
+
}
|
|
197
|
+
if (tls <= poor) {
|
|
198
|
+
return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'info', `TLS handshake could be faster (${tls}ms)`, { value: tls, recommendation: 'Consider TLS 1.3, HTTP/2, or preconnect hints' });
|
|
199
|
+
}
|
|
200
|
+
return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'warn', `Slow TLS handshake (${tls}ms)`, { value: tls, recommendation: 'TLS is slow. Check server configuration and certificate chain' });
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'response-html-size',
|
|
205
|
+
name: 'HTML Size',
|
|
206
|
+
category: 'performance',
|
|
207
|
+
severity: 'warning',
|
|
208
|
+
description: 'HTML should be under 500KB (ideally under 100KB)',
|
|
209
|
+
check: (ctx) => {
|
|
210
|
+
const size = ctx.htmlSize;
|
|
211
|
+
if (size === undefined)
|
|
212
|
+
return null;
|
|
213
|
+
const { good, warning, poor } = SEO_THRESHOLDS.responseSize.html;
|
|
214
|
+
const sizeKb = Math.round(size / 1024);
|
|
215
|
+
if (size <= good) {
|
|
216
|
+
return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'pass', `HTML size is good (${sizeKb}KB)`, { value: sizeKb });
|
|
217
|
+
}
|
|
218
|
+
if (size <= warning) {
|
|
219
|
+
return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'info', `HTML size is acceptable (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Consider reducing HTML size for faster parsing' });
|
|
220
|
+
}
|
|
221
|
+
if (size <= poor) {
|
|
222
|
+
return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'warn', `HTML is large (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Reduce HTML size. Check for inline data, remove unused code' });
|
|
223
|
+
}
|
|
224
|
+
return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'fail', `HTML is very large (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Critical: HTML too large. Use pagination, lazy loading, or split content' });
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'response-compression',
|
|
229
|
+
name: 'Compression',
|
|
230
|
+
category: 'performance',
|
|
231
|
+
severity: 'warning',
|
|
232
|
+
description: 'Response should be compressed (gzip/brotli)',
|
|
233
|
+
check: (ctx) => {
|
|
234
|
+
if (ctx.htmlSize === undefined)
|
|
235
|
+
return null;
|
|
236
|
+
if (ctx.isCompressed === false && ctx.htmlSize > 1024) {
|
|
237
|
+
return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'warn', 'Response is not compressed', { recommendation: 'Enable gzip or brotli compression on server' });
|
|
238
|
+
}
|
|
239
|
+
if (ctx.isCompressed === true) {
|
|
240
|
+
const ratio = ctx.compressedSize && ctx.htmlSize ? Math.round((ctx.compressedSize / ctx.htmlSize) * 100) : undefined;
|
|
241
|
+
return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'pass', ratio ? `Response compressed (${ratio}% of original)` : 'Response is compressed');
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const schemaRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'json-ld-exists',
|
|
5
|
+
name: 'Structured Data',
|
|
6
|
+
category: 'structured-data',
|
|
7
|
+
severity: 'info',
|
|
8
|
+
description: 'Page should have JSON-LD structured data',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (ctx.jsonLdCount === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
if (ctx.jsonLdCount === 0) {
|
|
13
|
+
return createResult({ id: 'json-ld-exists', name: 'Structured Data', category: 'structured-data', severity: 'info' }, 'info', 'No JSON-LD structured data found', { recommendation: 'Add Schema.org structured data for rich snippets' });
|
|
14
|
+
}
|
|
15
|
+
const types = ctx.jsonLdTypes?.join(', ') || 'unknown';
|
|
16
|
+
return createResult({ id: 'json-ld-exists', name: 'Structured Data', category: 'structured-data', severity: 'info' }, 'pass', `${ctx.jsonLdCount} JSON-LD block(s) found`, { value: types });
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'schema-standard-types',
|
|
21
|
+
name: 'Schema Types',
|
|
22
|
+
category: 'structured-data',
|
|
23
|
+
severity: 'info',
|
|
24
|
+
description: 'Use standard Schema.org types',
|
|
25
|
+
check: (ctx) => {
|
|
26
|
+
if (!ctx.jsonLdTypes || ctx.jsonLdTypes.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const recommended = [
|
|
29
|
+
'WebSite', 'WebPage', 'Article', 'Product', 'BreadcrumbList',
|
|
30
|
+
'Organization', 'Person', 'LocalBusiness', 'Recipe', 'Event',
|
|
31
|
+
'JobPosting', 'FAQPage', 'HowTo', 'VideoObject',
|
|
32
|
+
'SoftwareApplication', 'Review'
|
|
33
|
+
];
|
|
34
|
+
const hasStandard = ctx.jsonLdTypes.some(t => recommended.includes(t));
|
|
35
|
+
if (!hasStandard) {
|
|
36
|
+
return createResult({ id: 'schema-standard-types', name: 'Schema Types', category: 'structured-data', severity: 'info' }, 'info', 'Using uncommon Schema.org types', { value: ctx.jsonLdTypes.join(', '), recommendation: `Consider using standard types like ${recommended.slice(0, 3).join(', ')}` });
|
|
37
|
+
}
|
|
38
|
+
return createResult({ id: 'schema-standard-types', name: 'Schema Types', category: 'structured-data', severity: 'info' }, 'pass', 'Using standard Schema.org types', { value: ctx.jsonLdTypes.join(', ') });
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'breadcrumbs-presence',
|
|
43
|
+
name: 'Breadcrumbs Presence',
|
|
44
|
+
category: 'structured-data',
|
|
45
|
+
severity: 'info',
|
|
46
|
+
description: 'Breadcrumbs improve navigation and SEO structure',
|
|
47
|
+
check: (ctx) => {
|
|
48
|
+
if (!ctx.hasBreadcrumbsHtml && !ctx.hasBreadcrumbsSchema) {
|
|
49
|
+
return createResult({ id: 'breadcrumbs-presence', name: 'Breadcrumbs Presence', category: 'structured-data', severity: 'info' }, 'info', 'No breadcrumbs found (HTML or Schema.org)', { recommendation: 'Add breadcrumbs using HTML and/or Schema.org BreadcrumbList' });
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const securityRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'https-required',
|
|
5
|
+
name: 'HTTPS',
|
|
6
|
+
category: 'security',
|
|
7
|
+
severity: 'error',
|
|
8
|
+
description: 'Page must be served over HTTPS',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (ctx.isHttps === false) {
|
|
11
|
+
return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'fail', 'Page is not served over HTTPS', { recommendation: 'Enable HTTPS for all pages' });
|
|
12
|
+
}
|
|
13
|
+
if (ctx.isHttps === true) {
|
|
14
|
+
return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'pass', 'Page is served over HTTPS');
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'mixed-content',
|
|
21
|
+
name: 'Mixed Content',
|
|
22
|
+
category: 'security',
|
|
23
|
+
severity: 'error',
|
|
24
|
+
description: 'HTTPS pages should not load HTTP resources',
|
|
25
|
+
check: (ctx) => {
|
|
26
|
+
if (ctx.hasMixedContent) {
|
|
27
|
+
return createResult({ id: 'mixed-content', name: 'Mixed Content', category: 'security', severity: 'error' }, 'fail', 'Page has mixed content (HTTP resources on HTTPS page)', { recommendation: 'Update all resources to use HTTPS' });
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'security-csp-exists',
|
|
34
|
+
name: 'Content Security Policy (CSP)',
|
|
35
|
+
category: 'security',
|
|
36
|
+
severity: 'warning',
|
|
37
|
+
description: 'Content Security Policy header should be present to mitigate XSS attacks.',
|
|
38
|
+
check: (ctx) => {
|
|
39
|
+
if (!ctx.responseHeaders)
|
|
40
|
+
return null;
|
|
41
|
+
const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
|
|
42
|
+
if (!cspHeader) {
|
|
43
|
+
return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'warn', 'Content-Security-Policy header is missing', { recommendation: 'Implement a strong Content-Security-Policy to prevent XSS attacks.' });
|
|
44
|
+
}
|
|
45
|
+
return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'pass', 'Content-Security-Policy header is present');
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'security-xfo-exists',
|
|
50
|
+
name: 'X-Frame-Options',
|
|
51
|
+
category: 'security',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
description: 'X-Frame-Options header should be present to prevent clickjacking.',
|
|
54
|
+
check: (ctx) => {
|
|
55
|
+
if (!ctx.responseHeaders)
|
|
56
|
+
return null;
|
|
57
|
+
const xfoHeader = ctx.responseHeaders['x-frame-options'] || ctx.responseHeaders['X-Frame-Options'];
|
|
58
|
+
if (!xfoHeader) {
|
|
59
|
+
return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Frame-Options header is missing', { recommendation: 'Implement X-Frame-Options to prevent clickjacking attacks.' });
|
|
60
|
+
}
|
|
61
|
+
return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'pass', `X-Frame-Options header is present: ${xfoHeader}`);
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'security-cors-config',
|
|
66
|
+
name: 'CORS Configuration',
|
|
67
|
+
category: 'security',
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
description: 'Review Access-Control-Allow-Origin header for proper CORS configuration.',
|
|
70
|
+
check: (ctx) => {
|
|
71
|
+
if (!ctx.responseHeaders)
|
|
72
|
+
return null;
|
|
73
|
+
const acaoHeader = ctx.responseHeaders['access-control-allow-origin'] || ctx.responseHeaders['Access-Control-Allow-Origin'];
|
|
74
|
+
if (acaoHeader === '*') {
|
|
75
|
+
return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'warn', 'Access-Control-Allow-Origin is set to "*"', { recommendation: 'Avoid wildcard (*) in Access-Control-Allow-Origin for sensitive content. Specify allowed origins.' });
|
|
76
|
+
}
|
|
77
|
+
if (!acaoHeader) {
|
|
78
|
+
return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'info', 'Access-Control-Allow-Origin header is missing', { recommendation: 'Consider explicit CORS configuration if resources are consumed cross-origin.' });
|
|
79
|
+
}
|
|
80
|
+
return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'pass', `Access-Control-Allow-Origin: ${acaoHeader}`);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'security-hsts-exists',
|
|
85
|
+
name: 'Strict-Transport-Security (HSTS)',
|
|
86
|
+
category: 'security',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
description: 'HSTS header forces secure connections and improves SEO indirectly.',
|
|
89
|
+
check: (ctx) => {
|
|
90
|
+
if (!ctx.responseHeaders)
|
|
91
|
+
return null;
|
|
92
|
+
const hstsHeader = ctx.responseHeaders['strict-transport-security'] || ctx.responseHeaders['Strict-Transport-Security'];
|
|
93
|
+
if (!hstsHeader) {
|
|
94
|
+
return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'warn', 'Strict-Transport-Security header is missing', { recommendation: 'Implement HSTS to force secure connections and benefit SEO.' });
|
|
95
|
+
}
|
|
96
|
+
return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'pass', `Strict-Transport-Security header is present: ${hstsHeader}`);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'security-xcto-exists',
|
|
101
|
+
name: 'X-Content-Type-Options',
|
|
102
|
+
category: 'security',
|
|
103
|
+
severity: 'warning',
|
|
104
|
+
description: 'X-Content-Type-Options header prevents MIME sniffing attacks.',
|
|
105
|
+
check: (ctx) => {
|
|
106
|
+
if (!ctx.responseHeaders)
|
|
107
|
+
return null;
|
|
108
|
+
const xctoHeader = ctx.responseHeaders['x-content-type-options'] || ctx.responseHeaders['X-Content-Type-Options'];
|
|
109
|
+
if (!xctoHeader) {
|
|
110
|
+
return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Content-Type-Options header is missing', { recommendation: 'Implement X-Content-Type-Options: nosniff to prevent MIME sniffing.' });
|
|
111
|
+
}
|
|
112
|
+
return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'pass', `X-Content-Type-Options header is present: ${xctoHeader}`);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'security-rp-exists',
|
|
117
|
+
name: 'Referrer-Policy',
|
|
118
|
+
category: 'security',
|
|
119
|
+
severity: 'info',
|
|
120
|
+
description: 'Referrer-Policy controls how much referrer information is sent with requests.',
|
|
121
|
+
check: (ctx) => {
|
|
122
|
+
if (!ctx.responseHeaders)
|
|
123
|
+
return null;
|
|
124
|
+
const rpHeader = ctx.responseHeaders['referrer-policy'] || ctx.responseHeaders['Referrer-Policy'];
|
|
125
|
+
if (!rpHeader) {
|
|
126
|
+
return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'info', 'Referrer-Policy header is missing', { recommendation: 'Consider implementing a Referrer-Policy for better privacy and control (e.g., strict-origin-when-cross-origin).' });
|
|
127
|
+
}
|
|
128
|
+
return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'pass', `Referrer-Policy header is present: ${rpHeader}`);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'security-pp-exists',
|
|
133
|
+
name: 'Permissions-Policy',
|
|
134
|
+
category: 'security',
|
|
135
|
+
severity: 'info',
|
|
136
|
+
description: 'Permissions-Policy controls browser features available to the page and iframes.',
|
|
137
|
+
check: (ctx) => {
|
|
138
|
+
if (!ctx.responseHeaders)
|
|
139
|
+
return null;
|
|
140
|
+
const ppHeader = ctx.responseHeaders['permissions-policy'] || ctx.responseHeaders['Permissions-Policy'];
|
|
141
|
+
if (!ppHeader) {
|
|
142
|
+
return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'info', 'Permissions-Policy header is missing', { recommendation: 'Consider implementing a Permissions-Policy to disable unused browser features and enhance security (e.g., camera=(), microphone=()).' });
|
|
143
|
+
}
|
|
144
|
+
return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'pass', `Permissions-Policy header is present: ${ppHeader}`);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
import { SEO_THRESHOLDS } from './thresholds.js';
|
|
3
|
+
export const structuralRules = [
|
|
4
|
+
{
|
|
5
|
+
id: 'h1-exists',
|
|
6
|
+
name: 'H1 Exists',
|
|
7
|
+
category: 'headings',
|
|
8
|
+
severity: 'error',
|
|
9
|
+
description: 'Page must have exactly one H1 heading',
|
|
10
|
+
check: (ctx) => {
|
|
11
|
+
if (ctx.h1Count === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
if (ctx.h1Count === 0) {
|
|
14
|
+
return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'fail', 'Missing H1 heading', { recommendation: 'Add a single H1 heading that describes the page content' });
|
|
15
|
+
}
|
|
16
|
+
if (ctx.h1Count > 1) {
|
|
17
|
+
return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'warn', `Multiple H1 tags (${ctx.h1Count} found)`, { value: ctx.h1Count, recommendation: 'Use only one H1 per page' });
|
|
18
|
+
}
|
|
19
|
+
return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'pass', 'Single H1 tag present');
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'h1-length',
|
|
24
|
+
name: 'H1 Length',
|
|
25
|
+
category: 'headings',
|
|
26
|
+
severity: 'warning',
|
|
27
|
+
description: 'H1 should be 20-70 characters',
|
|
28
|
+
check: (ctx) => {
|
|
29
|
+
if (!ctx.h1Text)
|
|
30
|
+
return null;
|
|
31
|
+
const len = ctx.h1Length ?? ctx.h1Text.length;
|
|
32
|
+
const { minLength, maxLength } = SEO_THRESHOLDS.headings.h1;
|
|
33
|
+
if (len < minLength) {
|
|
34
|
+
return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too short (${len} chars, min: ${minLength})`, { value: len, recommendation: `Expand H1 to at least ${minLength} characters` });
|
|
35
|
+
}
|
|
36
|
+
if (len > maxLength) {
|
|
37
|
+
return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too long (${len} chars, max: ${maxLength})`, { value: len, recommendation: `Shorten H1 to under ${maxLength} characters` });
|
|
38
|
+
}
|
|
39
|
+
return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'pass', `H1 length OK (${len} chars)`, { value: len });
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'heading-hierarchy',
|
|
44
|
+
name: 'Heading Hierarchy',
|
|
45
|
+
category: 'headings',
|
|
46
|
+
severity: 'warning',
|
|
47
|
+
description: 'Headings should follow sequential order (H1 → H2 → H3)',
|
|
48
|
+
check: (ctx) => {
|
|
49
|
+
if (ctx.headingHierarchyValid === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
if (!ctx.headingHierarchyValid) {
|
|
52
|
+
const skipped = ctx.headingSkippedLevels?.join(', ') || 'unknown';
|
|
53
|
+
return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'warn', `Heading levels are skipped (${skipped})`, { recommendation: 'Use sequential heading levels (H1 → H2 → H3)' });
|
|
54
|
+
}
|
|
55
|
+
return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'pass', 'Heading structure is correct');
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'h2-count',
|
|
60
|
+
name: 'H2 Count',
|
|
61
|
+
category: 'headings',
|
|
62
|
+
severity: 'info',
|
|
63
|
+
description: 'Page should have 2-8 H2 headings for good structure',
|
|
64
|
+
check: (ctx) => {
|
|
65
|
+
if (ctx.h2Count === undefined)
|
|
66
|
+
return null;
|
|
67
|
+
const { min, max } = SEO_THRESHOLDS.headings.h2;
|
|
68
|
+
if (ctx.h2Count < min) {
|
|
69
|
+
return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'info', `Few H2 headings (${ctx.h2Count})`, { value: ctx.h2Count, recommendation: `Consider adding more H2 headings for better structure (${min}-${max} ideal)` });
|
|
70
|
+
}
|
|
71
|
+
if (ctx.h2Count > max) {
|
|
72
|
+
return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'info', `Many H2 headings (${ctx.h2Count})`, { value: ctx.h2Count, recommendation: 'Consider consolidating sections' });
|
|
73
|
+
}
|
|
74
|
+
return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'pass', `Good H2 count (${ctx.h2Count})`, { value: ctx.h2Count });
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'html5-header-exists',
|
|
79
|
+
name: 'HTML5 Header',
|
|
80
|
+
category: 'technical',
|
|
81
|
+
severity: 'info',
|
|
82
|
+
description: 'Page should use a <header> element',
|
|
83
|
+
check: (ctx) => {
|
|
84
|
+
if (!ctx.hasHeader) {
|
|
85
|
+
return createResult({ id: 'html5-header-exists', name: 'HTML5 Header', category: 'technical', severity: 'info' }, 'info', 'Missing <header> element', { recommendation: 'Use <header> for site-wide or page-specific introductory content' });
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'html5-nav-exists',
|
|
92
|
+
name: 'HTML5 Navigation',
|
|
93
|
+
category: 'technical',
|
|
94
|
+
severity: 'info',
|
|
95
|
+
description: 'Page should use a <nav> element for navigation links',
|
|
96
|
+
check: (ctx) => {
|
|
97
|
+
if (!ctx.hasNav) {
|
|
98
|
+
return createResult({ id: 'html5-nav-exists', name: 'HTML5 Navigation', category: 'technical', severity: 'info' }, 'info', 'Missing <nav> element', { recommendation: 'Use <nav> to define a block of navigation links' });
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'html5-main-exists',
|
|
105
|
+
name: 'HTML5 Main Content',
|
|
106
|
+
category: 'technical',
|
|
107
|
+
severity: 'info',
|
|
108
|
+
description: 'Page should use a <main> element for the dominant content',
|
|
109
|
+
check: (ctx) => {
|
|
110
|
+
if (!ctx.hasMain) {
|
|
111
|
+
return createResult({ id: 'html5-main-exists', name: 'HTML5 Main Content', category: 'technical', severity: 'info' }, 'info', 'Missing <main> element', { recommendation: 'Use <main> to enclose the dominant content of the <body>' });
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'html5-article-exists',
|
|
118
|
+
name: 'HTML5 Article',
|
|
119
|
+
category: 'technical',
|
|
120
|
+
severity: 'info',
|
|
121
|
+
description: 'Consider using <article> for independent, self-contained content',
|
|
122
|
+
check: (ctx) => {
|
|
123
|
+
if (!ctx.hasArticle && ctx.wordCount && ctx.wordCount > 500) {
|
|
124
|
+
return createResult({ id: 'html5-article-exists', name: 'HTML5 Article', category: 'technical', severity: 'info' }, 'info', 'Missing <article> element for substantial content', { recommendation: 'Consider using <article> for blog posts, news articles, etc.' });
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'html5-section-exists',
|
|
131
|
+
name: 'HTML5 Section',
|
|
132
|
+
category: 'technical',
|
|
133
|
+
severity: 'info',
|
|
134
|
+
description: 'Consider using <section> for thematic grouping of content',
|
|
135
|
+
check: (ctx) => {
|
|
136
|
+
if (!ctx.hasSection && (ctx.h2Count && ctx.h2Count > 1)) {
|
|
137
|
+
return createResult({ id: 'html5-section-exists', name: 'HTML5 Section', category: 'technical', severity: 'info' }, 'info', 'Missing <section> element for content grouping', { recommendation: 'Consider using <section> to group related content, usually with a heading' });
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'html5-footer-exists',
|
|
144
|
+
name: 'HTML5 Footer',
|
|
145
|
+
category: 'technical',
|
|
146
|
+
severity: 'info',
|
|
147
|
+
description: 'Page should use a <footer> element',
|
|
148
|
+
check: (ctx) => {
|
|
149
|
+
if (!ctx.hasFooter) {
|
|
150
|
+
return createResult({ id: 'html5-footer-exists', name: 'HTML5 Footer', category: 'technical', severity: 'info' }, 'info', 'Missing <footer> element', { recommendation: 'Use <footer> for site-wide or page-specific footer content' });
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|