recker 1.0.28 → 1.0.29
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/cli/tui/shell.d.ts +1 -0
- package/dist/cli/tui/shell.js +339 -5
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/spider.d.ts +61 -0
- package/dist/scrape/spider.js +250 -0
- package/dist/seo/analyzer.js +27 -0
- package/dist/seo/index.d.ts +3 -1
- package/dist/seo/index.js +1 -0
- package/dist/seo/rules/accessibility.js +620 -54
- package/dist/seo/rules/best-practices.d.ts +2 -0
- package/dist/seo/rules/best-practices.js +188 -0
- package/dist/seo/rules/crawl.d.ts +2 -0
- package/dist/seo/rules/crawl.js +307 -0
- package/dist/seo/rules/cwv.d.ts +2 -0
- package/dist/seo/rules/cwv.js +337 -0
- package/dist/seo/rules/ecommerce.d.ts +2 -0
- package/dist/seo/rules/ecommerce.js +252 -0
- package/dist/seo/rules/i18n.d.ts +2 -0
- package/dist/seo/rules/i18n.js +222 -0
- package/dist/seo/rules/index.d.ts +32 -0
- package/dist/seo/rules/index.js +71 -0
- package/dist/seo/rules/internal-linking.d.ts +2 -0
- package/dist/seo/rules/internal-linking.js +375 -0
- package/dist/seo/rules/local.d.ts +2 -0
- package/dist/seo/rules/local.js +265 -0
- package/dist/seo/rules/pwa.d.ts +2 -0
- package/dist/seo/rules/pwa.js +302 -0
- package/dist/seo/rules/readability.d.ts +2 -0
- package/dist/seo/rules/readability.js +255 -0
- package/dist/seo/rules/security.js +406 -28
- package/dist/seo/rules/social.d.ts +2 -0
- package/dist/seo/rules/social.js +373 -0
- package/dist/seo/rules/types.d.ts +155 -0
- package/dist/seo/seo-spider.d.ts +47 -0
- package/dist/seo/seo-spider.js +362 -0
- package/dist/seo/types.d.ts +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const socialRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'social-og-image-size',
|
|
5
|
+
name: 'OG Image Size',
|
|
6
|
+
category: 'og',
|
|
7
|
+
severity: 'warning',
|
|
8
|
+
description: 'Open Graph images should meet minimum size requirements',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (!ctx.ogImage)
|
|
11
|
+
return null;
|
|
12
|
+
if (!ctx.ogImageDimensions)
|
|
13
|
+
return null;
|
|
14
|
+
const { width, height } = ctx.ogImageDimensions;
|
|
15
|
+
const minWidth = 1200;
|
|
16
|
+
const minHeight = 630;
|
|
17
|
+
if (width < minWidth || height < minHeight) {
|
|
18
|
+
return createResult({ id: 'social-og-image-size', name: 'OG Image Size', category: 'og', severity: 'warning' }, 'warn', `OG image too small: ${width}x${height}`, {
|
|
19
|
+
recommendation: 'Use larger images for better social sharing',
|
|
20
|
+
evidence: {
|
|
21
|
+
found: `${width}x${height}px`,
|
|
22
|
+
expected: `Minimum ${minWidth}x${minHeight}px`,
|
|
23
|
+
impact: 'Small images may appear cropped or blurry on social platforms',
|
|
24
|
+
learnMore: 'https://developers.facebook.com/docs/sharing/best-practices/',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return createResult({ id: 'social-og-image-size', name: 'OG Image Size', category: 'og', severity: 'warning' }, 'pass', `OG image: ${width}x${height}px`);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'social-og-image-aspect-ratio',
|
|
33
|
+
name: 'OG Image Aspect Ratio',
|
|
34
|
+
category: 'og',
|
|
35
|
+
severity: 'info',
|
|
36
|
+
description: 'Open Graph images should use optimal aspect ratio',
|
|
37
|
+
check: (ctx) => {
|
|
38
|
+
if (!ctx.ogImageDimensions)
|
|
39
|
+
return null;
|
|
40
|
+
const { width, height } = ctx.ogImageDimensions;
|
|
41
|
+
const ratio = width / height;
|
|
42
|
+
const optimalRatio = 1.91;
|
|
43
|
+
const tolerance = 0.1;
|
|
44
|
+
if (Math.abs(ratio - optimalRatio) > tolerance) {
|
|
45
|
+
return createResult({ id: 'social-og-image-aspect-ratio', name: 'OG Image Aspect Ratio', category: 'og', severity: 'info' }, 'info', `OG image ratio: ${ratio.toFixed(2)}:1`, {
|
|
46
|
+
recommendation: 'Use 1.91:1 aspect ratio for optimal display',
|
|
47
|
+
evidence: {
|
|
48
|
+
found: `${ratio.toFixed(2)}:1`,
|
|
49
|
+
expected: '1.91:1 (e.g., 1200x628)',
|
|
50
|
+
impact: 'Non-optimal ratios may be cropped on social platforms',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return createResult({ id: 'social-og-image-aspect-ratio', name: 'OG Image Aspect Ratio', category: 'og', severity: 'info' }, 'pass', 'OG image has optimal aspect ratio');
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'social-og-locale',
|
|
59
|
+
name: 'OG Locale',
|
|
60
|
+
category: 'og',
|
|
61
|
+
severity: 'info',
|
|
62
|
+
description: 'Open Graph should specify content locale',
|
|
63
|
+
check: (ctx) => {
|
|
64
|
+
if (ctx.ogLocale === undefined)
|
|
65
|
+
return null;
|
|
66
|
+
if (!ctx.ogLocale) {
|
|
67
|
+
return createResult({ id: 'social-og-locale', name: 'OG Locale', category: 'og', severity: 'info' }, 'info', 'Missing og:locale', {
|
|
68
|
+
recommendation: 'Add og:locale for international content',
|
|
69
|
+
evidence: {
|
|
70
|
+
expected: '<meta property="og:locale" content="en_US">',
|
|
71
|
+
impact: 'Helps Facebook display content to correct language audience',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return createResult({ id: 'social-og-locale', name: 'OG Locale', category: 'og', severity: 'info' }, 'pass', `Locale: ${ctx.ogLocale}`);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'social-og-locale-alternate',
|
|
80
|
+
name: 'OG Locale Alternates',
|
|
81
|
+
category: 'og',
|
|
82
|
+
severity: 'info',
|
|
83
|
+
description: 'Multi-language sites should specify alternate locales',
|
|
84
|
+
check: (ctx) => {
|
|
85
|
+
if (!ctx.ogLocale)
|
|
86
|
+
return null;
|
|
87
|
+
if (!ctx.hreflangTags || ctx.hreflangTags.length <= 1)
|
|
88
|
+
return null;
|
|
89
|
+
if (ctx.ogLocaleAlternate === undefined)
|
|
90
|
+
return null;
|
|
91
|
+
if (!ctx.ogLocaleAlternate || ctx.ogLocaleAlternate.length === 0) {
|
|
92
|
+
return createResult({ id: 'social-og-locale-alternate', name: 'OG Locale Alternates', category: 'og', severity: 'info' }, 'info', 'Missing og:locale:alternate for multi-language site', {
|
|
93
|
+
recommendation: 'Add og:locale:alternate for other language versions',
|
|
94
|
+
evidence: {
|
|
95
|
+
found: `${ctx.hreflangTags.length} languages but no og:locale:alternate`,
|
|
96
|
+
expected: '<meta property="og:locale:alternate" content="es_ES">',
|
|
97
|
+
impact: 'Helps Facebook understand available language versions',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return createResult({ id: 'social-og-locale-alternate', name: 'OG Locale Alternates', category: 'og', severity: 'info' }, 'pass', `${ctx.ogLocaleAlternate.length} alternate locale(s)`);
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'social-og-article-tags',
|
|
106
|
+
name: 'OG Article Tags',
|
|
107
|
+
category: 'og',
|
|
108
|
+
severity: 'info',
|
|
109
|
+
description: 'Article pages should include article-specific Open Graph tags',
|
|
110
|
+
check: (ctx) => {
|
|
111
|
+
if (ctx.ogType !== 'article')
|
|
112
|
+
return null;
|
|
113
|
+
if (ctx.ogArticleTags === undefined)
|
|
114
|
+
return null;
|
|
115
|
+
const missing = [];
|
|
116
|
+
if (!ctx.ogArticlePublishedTime)
|
|
117
|
+
missing.push('article:published_time');
|
|
118
|
+
if (!ctx.ogArticleAuthor)
|
|
119
|
+
missing.push('article:author');
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
return createResult({ id: 'social-og-article-tags', name: 'OG Article Tags', category: 'og', severity: 'info' }, 'info', `Missing article OG tags: ${missing.join(', ')}`, {
|
|
122
|
+
recommendation: 'Add article-specific Open Graph tags',
|
|
123
|
+
evidence: {
|
|
124
|
+
found: missing,
|
|
125
|
+
expected: ['article:published_time', 'article:author', 'article:section', 'article:tag'],
|
|
126
|
+
impact: 'Rich article metadata improves social sharing appearance',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return createResult({ id: 'social-og-article-tags', name: 'OG Article Tags', category: 'og', severity: 'info' }, 'pass', 'Article OG tags present');
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'social-twitter-large-image',
|
|
135
|
+
name: 'Twitter Large Image',
|
|
136
|
+
category: 'twitter',
|
|
137
|
+
severity: 'info',
|
|
138
|
+
description: 'Consider using summary_large_image for better visibility',
|
|
139
|
+
check: (ctx) => {
|
|
140
|
+
if (!ctx.twitterCard)
|
|
141
|
+
return null;
|
|
142
|
+
if (ctx.twitterCard === 'summary') {
|
|
143
|
+
return createResult({ id: 'social-twitter-large-image', name: 'Twitter Large Image', category: 'twitter', severity: 'info' }, 'info', 'Using summary card (small image)', {
|
|
144
|
+
recommendation: 'Consider summary_large_image for more visual impact',
|
|
145
|
+
evidence: {
|
|
146
|
+
found: 'summary',
|
|
147
|
+
expected: 'summary_large_image for content-rich pages',
|
|
148
|
+
impact: 'Large images get 2x more engagement on Twitter',
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return createResult({ id: 'social-twitter-large-image', name: 'Twitter Large Image', category: 'twitter', severity: 'info' }, 'pass', `Twitter card: ${ctx.twitterCard}`);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'social-twitter-creator',
|
|
157
|
+
name: 'Twitter Creator',
|
|
158
|
+
category: 'twitter',
|
|
159
|
+
severity: 'info',
|
|
160
|
+
description: 'Article pages should include twitter:creator for attribution',
|
|
161
|
+
check: (ctx) => {
|
|
162
|
+
if (!ctx.twitterCard)
|
|
163
|
+
return null;
|
|
164
|
+
if (ctx.ogType !== 'article')
|
|
165
|
+
return null;
|
|
166
|
+
if (ctx.twitterCreator === undefined)
|
|
167
|
+
return null;
|
|
168
|
+
if (!ctx.twitterCreator) {
|
|
169
|
+
return createResult({ id: 'social-twitter-creator', name: 'Twitter Creator', category: 'twitter', severity: 'info' }, 'info', 'Missing twitter:creator on article', {
|
|
170
|
+
recommendation: 'Add twitter:creator for author attribution',
|
|
171
|
+
evidence: {
|
|
172
|
+
expected: '<meta name="twitter:creator" content="@username">',
|
|
173
|
+
impact: 'Attributes content to author and enables analytics',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return createResult({ id: 'social-twitter-creator', name: 'Twitter Creator', category: 'twitter', severity: 'info' }, 'pass', `Creator: ${ctx.twitterCreator}`);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'social-twitter-image-alt',
|
|
182
|
+
name: 'Twitter Image Alt',
|
|
183
|
+
category: 'twitter',
|
|
184
|
+
severity: 'info',
|
|
185
|
+
description: 'Twitter images should have alt text for accessibility',
|
|
186
|
+
check: (ctx) => {
|
|
187
|
+
if (!ctx.twitterImage)
|
|
188
|
+
return null;
|
|
189
|
+
if (ctx.twitterImageAlt === undefined)
|
|
190
|
+
return null;
|
|
191
|
+
if (!ctx.twitterImageAlt) {
|
|
192
|
+
return createResult({ id: 'social-twitter-image-alt', name: 'Twitter Image Alt', category: 'twitter', severity: 'info' }, 'info', 'Missing twitter:image:alt', {
|
|
193
|
+
recommendation: 'Add alt text for Twitter card image',
|
|
194
|
+
evidence: {
|
|
195
|
+
expected: '<meta name="twitter:image:alt" content="Description of image">',
|
|
196
|
+
impact: 'Improves accessibility for screen reader users',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return createResult({ id: 'social-twitter-image-alt', name: 'Twitter Image Alt', category: 'twitter', severity: 'info' }, 'pass', 'Twitter image has alt text');
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'social-linkedin-author',
|
|
205
|
+
name: 'LinkedIn Author',
|
|
206
|
+
category: 'og',
|
|
207
|
+
severity: 'info',
|
|
208
|
+
description: 'Professional content should include author information',
|
|
209
|
+
check: (ctx) => {
|
|
210
|
+
if (ctx.ogType !== 'article')
|
|
211
|
+
return null;
|
|
212
|
+
if (ctx.linkedinAuthor === undefined && ctx.ogArticleAuthor === undefined)
|
|
213
|
+
return null;
|
|
214
|
+
if (!ctx.linkedinAuthor && !ctx.ogArticleAuthor) {
|
|
215
|
+
return createResult({ id: 'social-linkedin-author', name: 'LinkedIn Author', category: 'og', severity: 'info' }, 'info', 'No author specified for LinkedIn', {
|
|
216
|
+
recommendation: 'Add author for professional network sharing',
|
|
217
|
+
evidence: {
|
|
218
|
+
expected: '<meta property="article:author" content="https://linkedin.com/in/author">',
|
|
219
|
+
impact: 'LinkedIn uses author info for professional attribution',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return createResult({ id: 'social-linkedin-author', name: 'LinkedIn Author', category: 'og', severity: 'info' }, 'pass', 'Author information present');
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'social-pinterest-rich-pins',
|
|
228
|
+
name: 'Pinterest Rich Pins',
|
|
229
|
+
category: 'og',
|
|
230
|
+
severity: 'info',
|
|
231
|
+
description: 'E-commerce and recipe sites should support Pinterest Rich Pins',
|
|
232
|
+
check: (ctx) => {
|
|
233
|
+
if (!ctx.isProductPage && ctx.ogType !== 'recipe')
|
|
234
|
+
return null;
|
|
235
|
+
if (ctx.pinterestRichPinSupport === undefined)
|
|
236
|
+
return null;
|
|
237
|
+
if (!ctx.pinterestRichPinSupport) {
|
|
238
|
+
return createResult({ id: 'social-pinterest-rich-pins', name: 'Pinterest Rich Pins', category: 'og', severity: 'info' }, 'info', 'No Pinterest Rich Pin support detected', {
|
|
239
|
+
recommendation: 'Add structured data for Pinterest Rich Pins',
|
|
240
|
+
evidence: {
|
|
241
|
+
expected: 'Product or Recipe schema.org markup',
|
|
242
|
+
impact: 'Rich Pins show real-time pricing and availability',
|
|
243
|
+
learnMore: 'https://developers.pinterest.com/docs/rich-pins/overview/',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return createResult({ id: 'social-pinterest-rich-pins', name: 'Pinterest Rich Pins', category: 'og', severity: 'info' }, 'pass', 'Pinterest Rich Pins supported');
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: 'social-pinterest-nopin',
|
|
252
|
+
name: 'Pinterest Nopin',
|
|
253
|
+
category: 'og',
|
|
254
|
+
severity: 'info',
|
|
255
|
+
description: 'Check for intentional Pinterest blocking',
|
|
256
|
+
check: (ctx) => {
|
|
257
|
+
if (ctx.hasPinterestNopin === undefined)
|
|
258
|
+
return null;
|
|
259
|
+
if (ctx.hasPinterestNopin) {
|
|
260
|
+
return createResult({ id: 'social-pinterest-nopin', name: 'Pinterest Nopin', category: 'og', severity: 'info' }, 'info', 'Pinterest pinning is disabled', {
|
|
261
|
+
evidence: {
|
|
262
|
+
found: 'data-pin-nopin or <meta name="pinterest" content="nopin">',
|
|
263
|
+
impact: 'Images cannot be pinned to Pinterest',
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'social-share-completeness',
|
|
272
|
+
name: 'Social Share Completeness',
|
|
273
|
+
category: 'og',
|
|
274
|
+
severity: 'warning',
|
|
275
|
+
description: 'Pages should have complete social sharing metadata',
|
|
276
|
+
check: (ctx) => {
|
|
277
|
+
const hasOg = ctx.ogTitle && ctx.ogDescription && ctx.ogImage;
|
|
278
|
+
const hasTwitter = ctx.twitterCard && (ctx.twitterTitle || ctx.ogTitle);
|
|
279
|
+
if (!hasOg && !hasTwitter) {
|
|
280
|
+
return createResult({ id: 'social-share-completeness', name: 'Social Share Completeness', category: 'og', severity: 'warning' }, 'warn', 'Missing social sharing metadata', {
|
|
281
|
+
recommendation: 'Add Open Graph and Twitter Card tags',
|
|
282
|
+
evidence: {
|
|
283
|
+
expected: ['og:title', 'og:description', 'og:image', 'twitter:card'],
|
|
284
|
+
impact: 'Without metadata, social platforms use generic previews',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (hasOg && !hasTwitter) {
|
|
289
|
+
return createResult({ id: 'social-share-completeness', name: 'Social Share Completeness', category: 'og', severity: 'warning' }, 'info', 'Has Open Graph but missing Twitter Card', {
|
|
290
|
+
recommendation: 'Add twitter:card for better Twitter previews',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return createResult({ id: 'social-share-completeness', name: 'Social Share Completeness', category: 'og', severity: 'warning' }, 'pass', 'Complete social metadata');
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'social-og-title-length',
|
|
298
|
+
name: 'OG Title Length',
|
|
299
|
+
category: 'og',
|
|
300
|
+
severity: 'info',
|
|
301
|
+
description: 'Open Graph titles should be optimized for social platforms',
|
|
302
|
+
check: (ctx) => {
|
|
303
|
+
if (!ctx.ogTitle)
|
|
304
|
+
return null;
|
|
305
|
+
const length = ctx.ogTitle.length;
|
|
306
|
+
const maxLength = 60;
|
|
307
|
+
if (length > maxLength) {
|
|
308
|
+
return createResult({ id: 'social-og-title-length', name: 'OG Title Length', category: 'og', severity: 'info' }, 'info', `OG title too long: ${length} chars`, {
|
|
309
|
+
recommendation: 'Shorten og:title for better display',
|
|
310
|
+
evidence: {
|
|
311
|
+
found: length,
|
|
312
|
+
expected: `Under ${maxLength} characters`,
|
|
313
|
+
impact: 'Long titles may be truncated on social platforms',
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return createResult({ id: 'social-og-title-length', name: 'OG Title Length', category: 'og', severity: 'info' }, 'pass', `OG title: ${length} chars`);
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'social-og-description-length',
|
|
322
|
+
name: 'OG Description Length',
|
|
323
|
+
category: 'og',
|
|
324
|
+
severity: 'info',
|
|
325
|
+
description: 'Open Graph descriptions should be optimized for social platforms',
|
|
326
|
+
check: (ctx) => {
|
|
327
|
+
if (!ctx.ogDescription)
|
|
328
|
+
return null;
|
|
329
|
+
const length = ctx.ogDescription.length;
|
|
330
|
+
const minLength = 55;
|
|
331
|
+
const maxLength = 200;
|
|
332
|
+
if (length < minLength) {
|
|
333
|
+
return createResult({ id: 'social-og-description-length', name: 'OG Description Length', category: 'og', severity: 'info' }, 'info', `OG description too short: ${length} chars`, {
|
|
334
|
+
recommendation: 'Expand og:description for better context',
|
|
335
|
+
evidence: {
|
|
336
|
+
found: length,
|
|
337
|
+
expected: `${minLength}-${maxLength} characters`,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (length > maxLength) {
|
|
342
|
+
return createResult({ id: 'social-og-description-length', name: 'OG Description Length', category: 'og', severity: 'info' }, 'info', `OG description long: ${length} chars`, {
|
|
343
|
+
evidence: {
|
|
344
|
+
found: length,
|
|
345
|
+
expected: `${minLength}-${maxLength} characters (may be truncated)`,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return createResult({ id: 'social-og-description-length', name: 'OG Description Length', category: 'og', severity: 'info' }, 'pass', `OG description: ${length} chars`);
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'social-fb-app-id',
|
|
354
|
+
name: 'Facebook App ID',
|
|
355
|
+
category: 'og',
|
|
356
|
+
severity: 'info',
|
|
357
|
+
description: 'Facebook App ID enables Insights and domain verification',
|
|
358
|
+
check: (ctx) => {
|
|
359
|
+
if (ctx.fbAppId === undefined)
|
|
360
|
+
return null;
|
|
361
|
+
if (!ctx.fbAppId) {
|
|
362
|
+
return createResult({ id: 'social-fb-app-id', name: 'Facebook App ID', category: 'og', severity: 'info' }, 'info', 'No Facebook App ID', {
|
|
363
|
+
recommendation: 'Add fb:app_id for Facebook Insights',
|
|
364
|
+
evidence: {
|
|
365
|
+
expected: '<meta property="fb:app_id" content="your-app-id">',
|
|
366
|
+
impact: 'Enables Facebook Insights and domain verification',
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return createResult({ id: 'social-fb-app-id', name: 'Facebook App ID', category: 'og', severity: 'info' }, 'pass', 'Facebook App ID present');
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
];
|
|
@@ -48,6 +48,22 @@ export interface RuleContext {
|
|
|
48
48
|
svgsWithoutTitle?: number;
|
|
49
49
|
interactiveElementsCount?: number;
|
|
50
50
|
ariaLabelledByMissing?: number;
|
|
51
|
+
elementsWithHighTabindex?: number;
|
|
52
|
+
invalidAriaAttributes?: number;
|
|
53
|
+
invalidAriaValues?: number;
|
|
54
|
+
invalidAriaRoles?: number;
|
|
55
|
+
missingRequiredAriaAttrs?: number;
|
|
56
|
+
hasAriaHiddenBody?: boolean;
|
|
57
|
+
ariaHiddenFocusableCount?: number;
|
|
58
|
+
deprecatedAriaRoles?: number;
|
|
59
|
+
duplicateAriaIds?: number;
|
|
60
|
+
dialogsWithoutName?: number;
|
|
61
|
+
hasSkipLink?: boolean;
|
|
62
|
+
videosWithCaptions?: number;
|
|
63
|
+
videosWithoutCaptions?: number;
|
|
64
|
+
invalidListStructure?: number;
|
|
65
|
+
emptyHeadings?: number;
|
|
66
|
+
imagesWithRedundantAlt?: number;
|
|
51
67
|
allLinks?: ExtractedLink[];
|
|
52
68
|
totalLinks?: number;
|
|
53
69
|
internalLinks?: number;
|
|
@@ -134,6 +150,12 @@ export interface RuleContext {
|
|
|
134
150
|
jsonLdTypes?: string[];
|
|
135
151
|
url?: string;
|
|
136
152
|
urlLength?: number;
|
|
153
|
+
hreflangTags?: Array<{
|
|
154
|
+
lang: string;
|
|
155
|
+
href: string;
|
|
156
|
+
}>;
|
|
157
|
+
ogLocale?: string;
|
|
158
|
+
alternateLanguages?: string[];
|
|
137
159
|
titleMatchesH1?: boolean;
|
|
138
160
|
urlHasUppercase?: boolean;
|
|
139
161
|
urlHasSpecialChars?: boolean;
|
|
@@ -153,6 +175,139 @@ export interface RuleContext {
|
|
|
153
175
|
htmlSize?: number;
|
|
154
176
|
compressedSize?: number;
|
|
155
177
|
isCompressed?: boolean;
|
|
178
|
+
isProductPage?: boolean;
|
|
179
|
+
productSchema?: {
|
|
180
|
+
name?: string;
|
|
181
|
+
image?: string | string[];
|
|
182
|
+
offers?: {
|
|
183
|
+
price?: number | string;
|
|
184
|
+
lowPrice?: number | string;
|
|
185
|
+
priceCurrency?: string;
|
|
186
|
+
availability?: string;
|
|
187
|
+
priceValidUntil?: string;
|
|
188
|
+
validFrom?: string;
|
|
189
|
+
validThrough?: string;
|
|
190
|
+
};
|
|
191
|
+
aggregateRating?: {
|
|
192
|
+
ratingValue?: number | string;
|
|
193
|
+
reviewCount?: number;
|
|
194
|
+
ratingCount?: number;
|
|
195
|
+
};
|
|
196
|
+
review?: unknown;
|
|
197
|
+
brand?: string | {
|
|
198
|
+
name?: string;
|
|
199
|
+
};
|
|
200
|
+
sku?: string;
|
|
201
|
+
gtin?: string;
|
|
202
|
+
gtin13?: string;
|
|
203
|
+
gtin14?: string;
|
|
204
|
+
gtin8?: string;
|
|
205
|
+
mpn?: string;
|
|
206
|
+
};
|
|
207
|
+
hasLocalBusinessSignals?: boolean;
|
|
208
|
+
localBusinessSchema?: {
|
|
209
|
+
'@type'?: string;
|
|
210
|
+
name?: string;
|
|
211
|
+
address?: {
|
|
212
|
+
streetAddress?: string;
|
|
213
|
+
addressLocality?: string;
|
|
214
|
+
addressRegion?: string;
|
|
215
|
+
postalCode?: string;
|
|
216
|
+
addressCountry?: string;
|
|
217
|
+
};
|
|
218
|
+
telephone?: string;
|
|
219
|
+
openingHoursSpecification?: unknown;
|
|
220
|
+
openingHours?: string | string[];
|
|
221
|
+
geo?: {
|
|
222
|
+
latitude?: number | string;
|
|
223
|
+
longitude?: number | string;
|
|
224
|
+
};
|
|
225
|
+
areaServed?: unknown;
|
|
226
|
+
priceRange?: string;
|
|
227
|
+
};
|
|
228
|
+
hasPhoneOnPage?: boolean;
|
|
229
|
+
hasAddressOnPage?: boolean;
|
|
230
|
+
lcpCandidate?: {
|
|
231
|
+
element?: string;
|
|
232
|
+
src?: string;
|
|
233
|
+
loading?: string;
|
|
234
|
+
fetchpriority?: string;
|
|
235
|
+
};
|
|
236
|
+
hasLcpPreload?: boolean;
|
|
237
|
+
webFonts?: Array<{
|
|
238
|
+
family?: string;
|
|
239
|
+
hasSwap?: boolean;
|
|
240
|
+
hasOptional?: boolean;
|
|
241
|
+
hasSizeAdjust?: boolean;
|
|
242
|
+
hasAscentOverride?: boolean;
|
|
243
|
+
}>;
|
|
244
|
+
renderBlockingStylesheets?: number;
|
|
245
|
+
renderBlockingScripts?: number;
|
|
246
|
+
hasAspectRatioCss?: boolean;
|
|
247
|
+
hasResponsiveImages?: boolean;
|
|
248
|
+
hasAdsWithoutReservedSpace?: boolean;
|
|
249
|
+
hasBannersWithoutMinHeight?: boolean;
|
|
250
|
+
hasInfiniteScroll?: boolean;
|
|
251
|
+
largeInlineScripts?: number;
|
|
252
|
+
inlineEventHandlers?: number;
|
|
253
|
+
hasHeavyAnimations?: boolean;
|
|
254
|
+
externalOrigins?: number;
|
|
255
|
+
hasCriticalResources?: boolean;
|
|
256
|
+
hasInlineCriticalCss?: boolean;
|
|
257
|
+
hasSitemapLink?: boolean;
|
|
258
|
+
sitemapUrl?: string;
|
|
259
|
+
robotsHasSitemap?: boolean;
|
|
260
|
+
isPaginatedPage?: boolean;
|
|
261
|
+
hasRelPrev?: boolean;
|
|
262
|
+
hasRelNext?: boolean;
|
|
263
|
+
hasDoctype?: boolean;
|
|
264
|
+
httpStatusCode?: number;
|
|
265
|
+
uncrawlableLinksCount?: number;
|
|
266
|
+
robotsTxtValid?: boolean;
|
|
267
|
+
robotsTxtError?: string;
|
|
268
|
+
structuredDataErrors?: number;
|
|
269
|
+
isIndexable?: boolean;
|
|
270
|
+
httpRedirectsToHttps?: boolean;
|
|
271
|
+
passiveVoicePercentage?: number;
|
|
272
|
+
transitionWordPercentage?: number;
|
|
273
|
+
consecutiveSentenceStarts?: number;
|
|
274
|
+
complexWordPercentage?: number;
|
|
275
|
+
hasManifest?: boolean;
|
|
276
|
+
manifestUrl?: string;
|
|
277
|
+
themeColor?: string;
|
|
278
|
+
hasAppleTouchIcon?: boolean;
|
|
279
|
+
hasAppleMobileWebAppCapable?: boolean;
|
|
280
|
+
appleStatusBarStyle?: string;
|
|
281
|
+
hasMaskableIcon?: boolean;
|
|
282
|
+
manifestStartUrl?: string;
|
|
283
|
+
manifestDisplay?: string;
|
|
284
|
+
manifestScope?: string;
|
|
285
|
+
manifestIconSizes?: number[];
|
|
286
|
+
manifestShortName?: string;
|
|
287
|
+
manifestName?: string;
|
|
288
|
+
manifestBackgroundColor?: string;
|
|
289
|
+
ogImageDimensions?: {
|
|
290
|
+
width: number;
|
|
291
|
+
height: number;
|
|
292
|
+
};
|
|
293
|
+
ogLocaleAlternate?: string[];
|
|
294
|
+
ogArticleTags?: string[];
|
|
295
|
+
ogArticlePublishedTime?: string;
|
|
296
|
+
ogArticleAuthor?: string;
|
|
297
|
+
twitterCreator?: string;
|
|
298
|
+
twitterImageAlt?: string;
|
|
299
|
+
linkedinAuthor?: string;
|
|
300
|
+
pinterestRichPinSupport?: boolean;
|
|
301
|
+
hasPinterestNopin?: boolean;
|
|
302
|
+
fbAppId?: string;
|
|
303
|
+
navLinkCount?: number;
|
|
304
|
+
footerLinkCount?: number;
|
|
305
|
+
contextualLinkCount?: number;
|
|
306
|
+
incomingInternalLinks?: number;
|
|
307
|
+
selfReferencingLinks?: number;
|
|
308
|
+
brokenInternalLinks?: number;
|
|
309
|
+
redirectChainLinks?: number;
|
|
310
|
+
pageClickDepth?: number;
|
|
156
311
|
}
|
|
157
312
|
export interface RuleEvidence {
|
|
158
313
|
found?: string | number | string[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SpiderOptions, SpiderResult, SpiderPageResult } from '../scrape/spider.js';
|
|
2
|
+
import type { SeoReport } from './types.js';
|
|
3
|
+
export interface SeoSpiderOptions extends SpiderOptions {
|
|
4
|
+
seo?: boolean;
|
|
5
|
+
output?: string;
|
|
6
|
+
onSeoAnalysis?: (result: SeoPageResult) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface SeoPageResult extends SpiderPageResult {
|
|
9
|
+
seoReport?: SeoReport;
|
|
10
|
+
}
|
|
11
|
+
export interface SiteWideIssue {
|
|
12
|
+
type: 'duplicate-title' | 'duplicate-description' | 'duplicate-h1' | 'missing-canonical' | 'orphan-page';
|
|
13
|
+
severity: 'error' | 'warning' | 'info';
|
|
14
|
+
message: string;
|
|
15
|
+
affectedUrls: string[];
|
|
16
|
+
value?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SeoSpiderResult extends Omit<SpiderResult, 'pages'> {
|
|
19
|
+
pages: SeoPageResult[];
|
|
20
|
+
siteWideIssues: SiteWideIssue[];
|
|
21
|
+
summary: {
|
|
22
|
+
totalPages: number;
|
|
23
|
+
pagesWithErrors: number;
|
|
24
|
+
pagesWithWarnings: number;
|
|
25
|
+
avgScore: number;
|
|
26
|
+
duplicateTitles: number;
|
|
27
|
+
duplicateDescriptions: number;
|
|
28
|
+
duplicateH1s: number;
|
|
29
|
+
orphanPages: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare class SeoSpider {
|
|
33
|
+
private spider;
|
|
34
|
+
private options;
|
|
35
|
+
private seoResults;
|
|
36
|
+
constructor(options?: SeoSpiderOptions);
|
|
37
|
+
crawl(startUrl: string): Promise<SeoSpiderResult>;
|
|
38
|
+
private analyzePages;
|
|
39
|
+
private createReportFromPageData;
|
|
40
|
+
private detectSiteWideIssues;
|
|
41
|
+
private calculateSummary;
|
|
42
|
+
private scoreToGrade;
|
|
43
|
+
private saveReport;
|
|
44
|
+
abort(): void;
|
|
45
|
+
isRunning(): boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare function seoSpider(url: string, options?: SeoSpiderOptions): Promise<SeoSpiderResult>;
|