@goflag/cli 0.2.8 → 0.2.10
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 +43 -8
- package/dist/{chunk-SKX7UCGC.js → chunk-DKGBLJ4H.js} +2004 -422
- package/dist/chunk-DKGBLJ4H.js.map +1 -0
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +96 -5
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/rules.json +653 -4
- package/dist/chunk-SKX7UCGC.js.map +0 -1
package/rules.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"counts": {
|
|
3
|
-
"page":
|
|
4
|
-
"site":
|
|
3
|
+
"page": 23,
|
|
4
|
+
"site": 26,
|
|
5
5
|
"prose": 4
|
|
6
6
|
},
|
|
7
7
|
"sources": {
|
|
8
|
+
"apple-web-apps": {
|
|
9
|
+
"id": "apple-web-apps",
|
|
10
|
+
"publisher": "Apple",
|
|
11
|
+
"title": "Configuring web applications (Safari Web Content Guide)",
|
|
12
|
+
"rigor": "vendor-spec",
|
|
13
|
+
"url": "https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html"
|
|
14
|
+
},
|
|
8
15
|
"bing-webmaster-guidelines": {
|
|
9
16
|
"id": "bing-webmaster-guidelines",
|
|
10
17
|
"publisher": "Microsoft",
|
|
@@ -19,6 +26,20 @@
|
|
|
19
26
|
"rigor": "vendor-spec",
|
|
20
27
|
"url": "https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls"
|
|
21
28
|
},
|
|
29
|
+
"google-hreflang": {
|
|
30
|
+
"id": "google-hreflang",
|
|
31
|
+
"publisher": "Google",
|
|
32
|
+
"title": "Localized versions of your pages (hreflang)",
|
|
33
|
+
"rigor": "vendor-spec",
|
|
34
|
+
"url": "https://developers.google.com/search/docs/specialty/international/localized-versions"
|
|
35
|
+
},
|
|
36
|
+
"google-robots-intro": {
|
|
37
|
+
"id": "google-robots-intro",
|
|
38
|
+
"publisher": "Google",
|
|
39
|
+
"title": "Introduction to robots.txt",
|
|
40
|
+
"rigor": "vendor-spec",
|
|
41
|
+
"url": "https://developers.google.com/search/docs/crawling-indexing/robots/intro"
|
|
42
|
+
},
|
|
22
43
|
"google-robots-meta": {
|
|
23
44
|
"id": "google-robots-meta",
|
|
24
45
|
"publisher": "Google",
|
|
@@ -26,6 +47,13 @@
|
|
|
26
47
|
"rigor": "vendor-spec",
|
|
27
48
|
"url": "https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag"
|
|
28
49
|
},
|
|
50
|
+
"google-sitemaps": {
|
|
51
|
+
"id": "google-sitemaps",
|
|
52
|
+
"publisher": "Google",
|
|
53
|
+
"title": "Build and submit a sitemap",
|
|
54
|
+
"rigor": "guideline",
|
|
55
|
+
"url": "https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview"
|
|
56
|
+
},
|
|
29
57
|
"google-snippet": {
|
|
30
58
|
"id": "google-snippet",
|
|
31
59
|
"publisher": "Google",
|
|
@@ -47,6 +75,20 @@
|
|
|
47
75
|
"rigor": "normative",
|
|
48
76
|
"url": "https://www.rfc-editor.org/rfc/rfc6596"
|
|
49
77
|
},
|
|
78
|
+
"ietf-rfc9309": {
|
|
79
|
+
"id": "ietf-rfc9309",
|
|
80
|
+
"publisher": "IETF",
|
|
81
|
+
"title": "RFC 9309 — Robots Exclusion Protocol",
|
|
82
|
+
"rigor": "normative",
|
|
83
|
+
"url": "https://www.rfc-editor.org/rfc/rfc9309"
|
|
84
|
+
},
|
|
85
|
+
"mdn-link-rel": {
|
|
86
|
+
"id": "mdn-link-rel",
|
|
87
|
+
"publisher": "MDN / Mozilla",
|
|
88
|
+
"title": "MDN — rel attribute values",
|
|
89
|
+
"rigor": "guideline",
|
|
90
|
+
"url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel"
|
|
91
|
+
},
|
|
50
92
|
"mdn-viewport": {
|
|
51
93
|
"id": "mdn-viewport",
|
|
52
94
|
"publisher": "MDN / Mozilla",
|
|
@@ -82,6 +124,20 @@
|
|
|
82
124
|
"rigor": "vendor-spec",
|
|
83
125
|
"url": "https://ogp.me/"
|
|
84
126
|
},
|
|
127
|
+
"sitemaps-protocol": {
|
|
128
|
+
"id": "sitemaps-protocol",
|
|
129
|
+
"publisher": "sitemaps.org",
|
|
130
|
+
"title": "Sitemaps XML protocol",
|
|
131
|
+
"rigor": "normative",
|
|
132
|
+
"url": "https://www.sitemaps.org/protocol.html"
|
|
133
|
+
},
|
|
134
|
+
"w3c-appmanifest": {
|
|
135
|
+
"id": "w3c-appmanifest",
|
|
136
|
+
"publisher": "W3C",
|
|
137
|
+
"title": "Web Application Manifest",
|
|
138
|
+
"rigor": "normative",
|
|
139
|
+
"url": "https://www.w3.org/TR/appmanifest/"
|
|
140
|
+
},
|
|
85
141
|
"w3c-i18n-language-tags": {
|
|
86
142
|
"id": "w3c-i18n-language-tags",
|
|
87
143
|
"publisher": "W3C",
|
|
@@ -96,6 +152,13 @@
|
|
|
96
152
|
"rigor": "normative",
|
|
97
153
|
"url": "https://html.spec.whatwg.org/multipage/dom.html"
|
|
98
154
|
},
|
|
155
|
+
"whatwg-html-link-types": {
|
|
156
|
+
"id": "whatwg-html-link-types",
|
|
157
|
+
"publisher": "WHATWG",
|
|
158
|
+
"title": "HTML Living Standard — link types",
|
|
159
|
+
"rigor": "normative",
|
|
160
|
+
"url": "https://html.spec.whatwg.org/multipage/links.html"
|
|
161
|
+
},
|
|
99
162
|
"whatwg-html-standard-metadata-names": {
|
|
100
163
|
"id": "whatwg-html-standard-metadata-names",
|
|
101
164
|
"publisher": "WHATWG",
|
|
@@ -116,6 +179,13 @@
|
|
|
116
179
|
"title": "URL Standard",
|
|
117
180
|
"rigor": "normative",
|
|
118
181
|
"url": "https://url.spec.whatwg.org/"
|
|
182
|
+
},
|
|
183
|
+
"x-cards": {
|
|
184
|
+
"id": "x-cards",
|
|
185
|
+
"publisher": "X (Twitter)",
|
|
186
|
+
"title": "X Cards — about cards markup",
|
|
187
|
+
"rigor": "vendor-spec",
|
|
188
|
+
"url": "https://developer.x.com/en/docs/twitter-for-websites/cards/overview/abouts-cards"
|
|
119
189
|
}
|
|
120
190
|
},
|
|
121
191
|
"rules": [
|
|
@@ -267,6 +337,135 @@
|
|
|
267
337
|
"rigor": null,
|
|
268
338
|
"sources": []
|
|
269
339
|
},
|
|
340
|
+
{
|
|
341
|
+
"id": "icons.apple-touch.missing",
|
|
342
|
+
"scope": "page",
|
|
343
|
+
"category": "icons",
|
|
344
|
+
"severity": "info",
|
|
345
|
+
"kind": "boolean",
|
|
346
|
+
"summary": "Declare an `apple-touch-icon` for the iOS home screen",
|
|
347
|
+
"why": "iOS does not read `rel=\"icon\"` for a home-screen shortcut. With no `apple-touch-icon` it screenshots the page instead, so what someone saved is a thumbnail of whatever was on screen when they saved it.",
|
|
348
|
+
"rigor": "vendor-spec",
|
|
349
|
+
"sources": [
|
|
350
|
+
"apple-web-apps",
|
|
351
|
+
"whatwg-html-link-types"
|
|
352
|
+
],
|
|
353
|
+
"reads": [
|
|
354
|
+
"links.icons",
|
|
355
|
+
"links.manifest"
|
|
356
|
+
],
|
|
357
|
+
"relates": [
|
|
358
|
+
"icons.missing"
|
|
359
|
+
],
|
|
360
|
+
"expected": "a `<link rel=\"apple-touch-icon\">`"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"id": "icons.ico.missing",
|
|
364
|
+
"scope": "site",
|
|
365
|
+
"severity": "info",
|
|
366
|
+
"kind": "boolean",
|
|
367
|
+
"summary": "Serve a `/favicon.ico` at the root for the clients that ask blind",
|
|
368
|
+
"rigor": "guideline",
|
|
369
|
+
"sources": [
|
|
370
|
+
"whatwg-html-link-types",
|
|
371
|
+
"mdn-link-rel"
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"id": "icons.manifest-mismatch",
|
|
376
|
+
"scope": "page",
|
|
377
|
+
"category": "icons",
|
|
378
|
+
"severity": "info",
|
|
379
|
+
"kind": "boolean",
|
|
380
|
+
"summary": "The manifest and the `<head>` must not disagree about the same icon",
|
|
381
|
+
"why": "Two files declare the icons, maintained by different hands, and nothing compares them. The failure worth naming is not that the two lists differ — they legitimately do, since an apple-touch-icon is not a manifest icon — but that they describe the *same file* differently, or that the icons exist only in the manifest, where a browser tab never looks.",
|
|
382
|
+
"rigor": "guideline",
|
|
383
|
+
"sources": [
|
|
384
|
+
"w3c-appmanifest",
|
|
385
|
+
"whatwg-html-link-types"
|
|
386
|
+
],
|
|
387
|
+
"reads": [
|
|
388
|
+
"links.icons",
|
|
389
|
+
"links.manifest"
|
|
390
|
+
],
|
|
391
|
+
"relates": [
|
|
392
|
+
"icons.missing"
|
|
393
|
+
],
|
|
394
|
+
"expected": "the same size and type wherever an icon is declared twice"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"id": "icons.missing",
|
|
398
|
+
"scope": "page",
|
|
399
|
+
"category": "icons",
|
|
400
|
+
"severity": "warning",
|
|
401
|
+
"kind": "boolean",
|
|
402
|
+
"summary": "Declare an icon — the tab, the bookmark and the share sheet all read one",
|
|
403
|
+
"why": "No specification requires an icon, which is exactly why nothing complains and every consumer improvises. A browser falls back to `/favicon.ico` at the root, a feed reader or a link unfurler often falls back to nothing, and a bookmark to a site with no icon is a grey square among fifty.",
|
|
404
|
+
"rigor": "guideline",
|
|
405
|
+
"sources": [
|
|
406
|
+
"whatwg-html-link-types",
|
|
407
|
+
"mdn-link-rel"
|
|
408
|
+
],
|
|
409
|
+
"reads": [
|
|
410
|
+
"links.icons",
|
|
411
|
+
"links.manifest"
|
|
412
|
+
],
|
|
413
|
+
"relates": [
|
|
414
|
+
"icons.apple-touch.missing",
|
|
415
|
+
"icons.manifest-mismatch"
|
|
416
|
+
],
|
|
417
|
+
"expected": "at least one `<link rel=\"icon\">`, or icons declared by a manifest",
|
|
418
|
+
"fix": {
|
|
419
|
+
"title": "Let the file convention declare it",
|
|
420
|
+
"snippet": "// app/icon.svg (or icon.png) — Next emits the <link rel=\"icon\"> itself.\n// app/apple-icon.png likewise emits rel=\"apple-touch-icon\".\n// A /favicon.ico at the root is a separate matter: see icons.ico.missing.",
|
|
421
|
+
"language": "tsx"
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"id": "icons.sizes-mismatch",
|
|
426
|
+
"scope": "page",
|
|
427
|
+
"category": "icons",
|
|
428
|
+
"severity": "info",
|
|
429
|
+
"kind": "boolean",
|
|
430
|
+
"summary": "A `sizes` attribute must describe the file it points at",
|
|
431
|
+
"why": "`sizes` is how a client picks one icon out of several without fetching them all, so a wrong value costs exactly what the attribute was there to save: the client downloads the file it was told to prefer and gets the wrong resolution. A half-true declaration is the common shape — a `.ico` carrying 16, 32 and 48 declared as `48x48` advertises one third of itself.",
|
|
432
|
+
"rigor": "guideline",
|
|
433
|
+
"sources": [
|
|
434
|
+
"whatwg-html-link-types",
|
|
435
|
+
"mdn-link-rel"
|
|
436
|
+
],
|
|
437
|
+
"reads": [
|
|
438
|
+
"links.icons",
|
|
439
|
+
"assets"
|
|
440
|
+
],
|
|
441
|
+
"relates": [
|
|
442
|
+
"icons.unreachable"
|
|
443
|
+
],
|
|
444
|
+
"expected": "`sizes` listing what the file actually contains"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"id": "icons.unreachable",
|
|
448
|
+
"scope": "page",
|
|
449
|
+
"category": "icons",
|
|
450
|
+
"severity": "warning",
|
|
451
|
+
"kind": "boolean",
|
|
452
|
+
"summary": "A declared icon has to answer with an image",
|
|
453
|
+
"why": "An icon that 404s is worse than one never declared: the client asks, gets nothing, and has already skipped the `/favicon.ico` it would have fallen back to. The declaration is what took the fallback away.",
|
|
454
|
+
"rigor": "vendor-spec",
|
|
455
|
+
"sources": [
|
|
456
|
+
"whatwg-html-link-types",
|
|
457
|
+
"mdn-link-rel"
|
|
458
|
+
],
|
|
459
|
+
"reads": [
|
|
460
|
+
"links.icons",
|
|
461
|
+
"assets"
|
|
462
|
+
],
|
|
463
|
+
"relates": [
|
|
464
|
+
"icons.missing",
|
|
465
|
+
"icons.ico.missing"
|
|
466
|
+
],
|
|
467
|
+
"expected": "a 2xx image at every declared icon URL"
|
|
468
|
+
},
|
|
270
469
|
{
|
|
271
470
|
"id": "lang.matches-content",
|
|
272
471
|
"scope": "prose",
|
|
@@ -312,6 +511,85 @@
|
|
|
312
511
|
],
|
|
313
512
|
"expected": "an `og:description` alongside the other `og:*` tags"
|
|
314
513
|
},
|
|
514
|
+
{
|
|
515
|
+
"id": "og.image.absolute",
|
|
516
|
+
"scope": "page",
|
|
517
|
+
"category": "opengraph",
|
|
518
|
+
"severity": "warning",
|
|
519
|
+
"kind": "boolean",
|
|
520
|
+
"summary": "`og:image` must be an absolute URL",
|
|
521
|
+
"why": "Unlike a browser, the crawler that builds the preview has no document to resolve a relative path against — it reads the tag, not the page. A relative `og:image` is therefore not a smaller mistake than a missing one: the unfurl is identical, and the tag makes it look handled.",
|
|
522
|
+
"rigor": "vendor-spec",
|
|
523
|
+
"sources": [
|
|
524
|
+
"ogp",
|
|
525
|
+
"meta-og-sharing"
|
|
526
|
+
],
|
|
527
|
+
"reads": [
|
|
528
|
+
"openGraph.images"
|
|
529
|
+
],
|
|
530
|
+
"relates": [
|
|
531
|
+
"og.image.missing",
|
|
532
|
+
"canonical.absolute"
|
|
533
|
+
],
|
|
534
|
+
"expected": "every `og:image` URL absolute, scheme included",
|
|
535
|
+
"fix": {
|
|
536
|
+
"title": "Give Next a metadataBase, or write the origin in",
|
|
537
|
+
"snippet": "// app/layout.tsx — every relative metadata URL resolves against this.\nexport const metadata = {\n metadataBase: new URL(process.env.SITE_ORIGIN!),\n openGraph: { images: [\"/og.png\"] }, // emitted absolute\n};",
|
|
538
|
+
"language": "tsx"
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"id": "og.image.alt",
|
|
543
|
+
"scope": "page",
|
|
544
|
+
"category": "opengraph",
|
|
545
|
+
"severity": "warning",
|
|
546
|
+
"kind": "boolean",
|
|
547
|
+
"summary": "Describe the shared image with `og:image:alt`",
|
|
548
|
+
"why": "The protocol defines `og:image:alt` as a description of what is in the image, and a generated card usually carries the page's title as pixels. Omitting it leaves that text sighted-only, in the one place a link is seen before anybody has opened the page. Optional in the vocabulary, which is why this is a guideline and not a spec violation.",
|
|
549
|
+
"rigor": "guideline",
|
|
550
|
+
"sources": [
|
|
551
|
+
"ogp"
|
|
552
|
+
],
|
|
553
|
+
"reads": [
|
|
554
|
+
"openGraph.images"
|
|
555
|
+
],
|
|
556
|
+
"relates": [
|
|
557
|
+
"og.image.missing"
|
|
558
|
+
],
|
|
559
|
+
"expected": "an `og:image:alt` beside every `og:image`",
|
|
560
|
+
"fix": {
|
|
561
|
+
"title": "Carry the alt per image, not as a constant",
|
|
562
|
+
"snippet": "// app/…/opengraph-image.tsx — `export const alt` is one fixed string, so a\n// translated or data-derived description has to go through the metadata fn.\nexport async function generateImageMetadata({ params }) {\n const t = await getTranslations({ locale: params.locale });\n return [{ id: \"og\", size, contentType, alt: t(\"og.alt\") }];\n}",
|
|
563
|
+
"language": "tsx"
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
"id": "og.image.dimensions",
|
|
568
|
+
"scope": "page",
|
|
569
|
+
"category": "opengraph",
|
|
570
|
+
"severity": "warning",
|
|
571
|
+
"kind": "boolean",
|
|
572
|
+
"summary": "Declare `og:image:width` and `og:image:height`",
|
|
573
|
+
"why": "A crawler that does not know the size cannot lay the card out until it has fetched the image, so the first share of a URL renders without it — the one share that matters most. The dimensions are the fix, and they cost two tags.",
|
|
574
|
+
"rigor": "vendor-spec",
|
|
575
|
+
"sources": [
|
|
576
|
+
"ogp",
|
|
577
|
+
"meta-og-sharing"
|
|
578
|
+
],
|
|
579
|
+
"reads": [
|
|
580
|
+
"openGraph.images"
|
|
581
|
+
],
|
|
582
|
+
"relates": [
|
|
583
|
+
"og.image.missing",
|
|
584
|
+
"og.image.ratio"
|
|
585
|
+
],
|
|
586
|
+
"expected": "both `og:image:width` and `og:image:height` on every image",
|
|
587
|
+
"fix": {
|
|
588
|
+
"title": "Declare the size alongside the URL",
|
|
589
|
+
"snippet": "// app/…/page.tsx\nexport const metadata = {\n openGraph: { images: [{ url: \"/og.png\", width: 1200, height: 630 }] },\n};\n// The file convention emits both from `export const size` for free.",
|
|
590
|
+
"language": "tsx"
|
|
591
|
+
}
|
|
592
|
+
},
|
|
315
593
|
{
|
|
316
594
|
"id": "og.image.missing",
|
|
317
595
|
"scope": "page",
|
|
@@ -338,6 +616,64 @@
|
|
|
338
616
|
"language": "tsx"
|
|
339
617
|
}
|
|
340
618
|
},
|
|
619
|
+
{
|
|
620
|
+
"id": "og.image.ratio",
|
|
621
|
+
"scope": "page",
|
|
622
|
+
"category": "opengraph",
|
|
623
|
+
"severity": "info",
|
|
624
|
+
"kind": "scored",
|
|
625
|
+
"summary": "Keep the shared image near the 1.91:1 the card is laid out for",
|
|
626
|
+
"why": "Meta's recommended 1200×630 is a shape before it is a size. An image far from it is not rejected — it is cropped to fit, and the crop is chosen by the consumer rather than by you.",
|
|
627
|
+
"rigor": "vendor-spec",
|
|
628
|
+
"sources": [
|
|
629
|
+
"meta-og-sharing",
|
|
630
|
+
"x-cards"
|
|
631
|
+
],
|
|
632
|
+
"reads": [
|
|
633
|
+
"openGraph.images"
|
|
634
|
+
],
|
|
635
|
+
"relates": [
|
|
636
|
+
"og.image.dimensions"
|
|
637
|
+
],
|
|
638
|
+
"expected": "a width ÷ height between 1.7 and 2.1 (1.91:1 is 1.9)",
|
|
639
|
+
"bands": {
|
|
640
|
+
"ideal": [
|
|
641
|
+
1.7,
|
|
642
|
+
2.1
|
|
643
|
+
],
|
|
644
|
+
"acceptable": [
|
|
645
|
+
1,
|
|
646
|
+
3
|
|
647
|
+
]
|
|
648
|
+
},
|
|
649
|
+
"severityByBand": {
|
|
650
|
+
"acceptable": "info",
|
|
651
|
+
"poor": "warning"
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"id": "og.image.reachable",
|
|
656
|
+
"scope": "page",
|
|
657
|
+
"category": "opengraph",
|
|
658
|
+
"severity": "error",
|
|
659
|
+
"kind": "boolean",
|
|
660
|
+
"summary": "The URL in `og:image` has to answer with an image",
|
|
661
|
+
"why": "Every other check on a preview image judges the tag. This one judges the file, and it is the only one that can catch the failure with no symptom: a URL that is present, well-formed, absolute — and dead. The card renders empty and nothing in the page says why. Found on this project's own documentation, where eleven pages pointed at a route a redirect had been swallowing since the day it was written.",
|
|
662
|
+
"rigor": "vendor-spec",
|
|
663
|
+
"sources": [
|
|
664
|
+
"ogp",
|
|
665
|
+
"meta-og-sharing"
|
|
666
|
+
],
|
|
667
|
+
"reads": [
|
|
668
|
+
"openGraph.images",
|
|
669
|
+
"assets"
|
|
670
|
+
],
|
|
671
|
+
"relates": [
|
|
672
|
+
"og.image.missing",
|
|
673
|
+
"og.image.absolute"
|
|
674
|
+
],
|
|
675
|
+
"expected": "a 2xx image at every `og:image` URL"
|
|
676
|
+
},
|
|
341
677
|
{
|
|
342
678
|
"id": "og.image.representative",
|
|
343
679
|
"scope": "prose",
|
|
@@ -359,6 +695,62 @@
|
|
|
359
695
|
],
|
|
360
696
|
"prose": "Does the og:image represent what this page is about, rather than being a site-wide default, and does its subject survive being cropped to the 1.91:1 aspect ratio consumers render?"
|
|
361
697
|
},
|
|
698
|
+
{
|
|
699
|
+
"id": "og.locale.alternates",
|
|
700
|
+
"scope": "page",
|
|
701
|
+
"category": "opengraph",
|
|
702
|
+
"severity": "warning",
|
|
703
|
+
"kind": "boolean",
|
|
704
|
+
"summary": "`og:locale:alternate` and the hreflang cluster must name the same locales",
|
|
705
|
+
"why": "Two vocabularies describe the same fact — this page exists in these languages — and nothing in a build keeps them in step, so they drift silently. Whichever one is short is the one telling a consumer that a translation it could have linked to does not exist.",
|
|
706
|
+
"rigor": "vendor-spec",
|
|
707
|
+
"sources": [
|
|
708
|
+
"ogp",
|
|
709
|
+
"google-hreflang"
|
|
710
|
+
],
|
|
711
|
+
"reads": [
|
|
712
|
+
"openGraph.locale",
|
|
713
|
+
"openGraph.localeAlternates",
|
|
714
|
+
"links.hreflang"
|
|
715
|
+
],
|
|
716
|
+
"relates": [
|
|
717
|
+
"og.locale.missing",
|
|
718
|
+
"hreflang.missing"
|
|
719
|
+
],
|
|
720
|
+
"expected": "one `og:locale:alternate` per hreflang locale other than this page's own",
|
|
721
|
+
"fix": {
|
|
722
|
+
"title": "Derive both lists from the same cluster",
|
|
723
|
+
"snippet": "// The hreflang map and the OG locales are one fact stated twice.\nopenGraph: {\n locale: ogLocale(locale),\n alternateLocale: cluster.filter((l) => l !== locale).map(ogLocale),\n},\nalternates: { languages: cluster },",
|
|
724
|
+
"language": "ts"
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
"id": "og.locale.missing",
|
|
729
|
+
"scope": "page",
|
|
730
|
+
"category": "opengraph",
|
|
731
|
+
"severity": "warning",
|
|
732
|
+
"kind": "boolean",
|
|
733
|
+
"summary": "A translated page has to say which locale it is in",
|
|
734
|
+
"why": "`og:locale` defaults to `en_US` in the protocol itself — so a page that omits it does not decline to answer, it answers wrongly. On a site whose whole point is that the same page exists in several languages, every translation claims to be American English.",
|
|
735
|
+
"rigor": "vendor-spec",
|
|
736
|
+
"sources": [
|
|
737
|
+
"ogp"
|
|
738
|
+
],
|
|
739
|
+
"reads": [
|
|
740
|
+
"openGraph.locale",
|
|
741
|
+
"links.hreflang"
|
|
742
|
+
],
|
|
743
|
+
"relates": [
|
|
744
|
+
"og.locale.alternates",
|
|
745
|
+
"hreflang.missing"
|
|
746
|
+
],
|
|
747
|
+
"expected": "an `og:locale` on any page that declares hreflang alternates",
|
|
748
|
+
"fix": {
|
|
749
|
+
"title": "Emit the locale in the page's own metadata",
|
|
750
|
+
"snippet": "// app/[locale]/…/page.tsx\nexport const metadata = {\n openGraph: { locale: \"fr_FR\", alternateLocale: [\"en_US\", \"es_ES\"] },\n};",
|
|
751
|
+
"language": "tsx"
|
|
752
|
+
}
|
|
753
|
+
},
|
|
362
754
|
{
|
|
363
755
|
"id": "og.title.missing",
|
|
364
756
|
"scope": "page",
|
|
@@ -380,14 +772,29 @@
|
|
|
380
772
|
],
|
|
381
773
|
"expected": "an explicit `og:title`"
|
|
382
774
|
},
|
|
775
|
+
{
|
|
776
|
+
"id": "robots.blocks-page",
|
|
777
|
+
"scope": "site",
|
|
778
|
+
"severity": "error",
|
|
779
|
+
"kind": "boolean",
|
|
780
|
+
"summary": "A page asking to be indexed must not be disallowed by robots.txt",
|
|
781
|
+
"rigor": "vendor-spec",
|
|
782
|
+
"sources": [
|
|
783
|
+
"ietf-rfc9309",
|
|
784
|
+
"google-robots-intro"
|
|
785
|
+
]
|
|
786
|
+
},
|
|
383
787
|
{
|
|
384
788
|
"id": "robots.blocks-site",
|
|
385
789
|
"scope": "site",
|
|
386
790
|
"severity": "error",
|
|
387
791
|
"kind": "boolean",
|
|
388
792
|
"summary": "`robots.txt` must not forbid crawling a site that asks to be indexed",
|
|
389
|
-
"rigor":
|
|
390
|
-
"sources": [
|
|
793
|
+
"rigor": "vendor-spec",
|
|
794
|
+
"sources": [
|
|
795
|
+
"ietf-rfc9309",
|
|
796
|
+
"google-robots-intro"
|
|
797
|
+
]
|
|
391
798
|
},
|
|
392
799
|
{
|
|
393
800
|
"id": "robots.conflict",
|
|
@@ -408,6 +815,248 @@
|
|
|
408
815
|
],
|
|
409
816
|
"expected": "consistent indexing directives across meta tags and headers"
|
|
410
817
|
},
|
|
818
|
+
{
|
|
819
|
+
"id": "robotstxt.cross-origin",
|
|
820
|
+
"scope": "site",
|
|
821
|
+
"severity": "warning",
|
|
822
|
+
"kind": "boolean",
|
|
823
|
+
"summary": "`/robots.txt` should not redirect to another origin",
|
|
824
|
+
"rigor": "vendor-spec",
|
|
825
|
+
"sources": [
|
|
826
|
+
"ietf-rfc9309"
|
|
827
|
+
]
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"id": "robotstxt.invalid-line",
|
|
831
|
+
"scope": "site",
|
|
832
|
+
"severity": "warning",
|
|
833
|
+
"kind": "boolean",
|
|
834
|
+
"summary": "Every line of robots.txt should parse as something",
|
|
835
|
+
"rigor": "spec-required",
|
|
836
|
+
"sources": [
|
|
837
|
+
"ietf-rfc9309"
|
|
838
|
+
]
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"id": "robotstxt.oversized",
|
|
842
|
+
"scope": "site",
|
|
843
|
+
"severity": "error",
|
|
844
|
+
"kind": "boolean",
|
|
845
|
+
"summary": "Rules past 500 KiB of robots.txt are not guaranteed to be read",
|
|
846
|
+
"rigor": "spec-required",
|
|
847
|
+
"sources": [
|
|
848
|
+
"ietf-rfc9309"
|
|
849
|
+
]
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"id": "robotstxt.sitemap.relative",
|
|
853
|
+
"scope": "site",
|
|
854
|
+
"severity": "error",
|
|
855
|
+
"kind": "boolean",
|
|
856
|
+
"summary": "A `Sitemap:` declaration must be an absolute URL",
|
|
857
|
+
"rigor": "spec-required",
|
|
858
|
+
"sources": [
|
|
859
|
+
"sitemaps-protocol",
|
|
860
|
+
"ietf-rfc9309"
|
|
861
|
+
]
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
"id": "robotstxt.unknown-directive",
|
|
865
|
+
"scope": "site",
|
|
866
|
+
"severity": "info",
|
|
867
|
+
"kind": "boolean",
|
|
868
|
+
"summary": "Non-standard robots.txt directives are read by some crawlers and ignored by others",
|
|
869
|
+
"rigor": "guideline",
|
|
870
|
+
"sources": [
|
|
871
|
+
"ietf-rfc9309",
|
|
872
|
+
"google-robots-intro"
|
|
873
|
+
]
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"id": "robotstxt.unreachable",
|
|
877
|
+
"scope": "site",
|
|
878
|
+
"severity": "error",
|
|
879
|
+
"kind": "boolean",
|
|
880
|
+
"summary": "A robots.txt that errors is read as forbidding the whole site",
|
|
881
|
+
"rigor": "spec-required",
|
|
882
|
+
"sources": [
|
|
883
|
+
"ietf-rfc9309"
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
"id": "sitemap.empty",
|
|
888
|
+
"scope": "site",
|
|
889
|
+
"severity": "warning",
|
|
890
|
+
"kind": "boolean",
|
|
891
|
+
"summary": "A sitemap that parses should list something",
|
|
892
|
+
"rigor": "guideline",
|
|
893
|
+
"sources": [
|
|
894
|
+
"sitemaps-protocol"
|
|
895
|
+
]
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
"id": "sitemap.entry.blocked-by-robots",
|
|
899
|
+
"scope": "site",
|
|
900
|
+
"severity": "error",
|
|
901
|
+
"kind": "boolean",
|
|
902
|
+
"summary": "A sitemap must not list URLs that robots.txt forbids fetching",
|
|
903
|
+
"rigor": "vendor-spec",
|
|
904
|
+
"sources": [
|
|
905
|
+
"ietf-rfc9309",
|
|
906
|
+
"sitemaps-protocol"
|
|
907
|
+
]
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
"id": "sitemap.entry.cross-host",
|
|
911
|
+
"scope": "site",
|
|
912
|
+
"severity": "error",
|
|
913
|
+
"kind": "boolean",
|
|
914
|
+
"summary": "A sitemap should only list URLs on its own host",
|
|
915
|
+
"rigor": "spec-required",
|
|
916
|
+
"sources": [
|
|
917
|
+
"sitemaps-protocol"
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
"id": "sitemap.entry.invalid-url",
|
|
922
|
+
"scope": "site",
|
|
923
|
+
"severity": "error",
|
|
924
|
+
"kind": "boolean",
|
|
925
|
+
"summary": "Every `<loc>` must be an absolute, parseable URL",
|
|
926
|
+
"rigor": "spec-required",
|
|
927
|
+
"sources": [
|
|
928
|
+
"sitemaps-protocol"
|
|
929
|
+
]
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
"id": "sitemap.entry.noindex",
|
|
933
|
+
"scope": "site",
|
|
934
|
+
"severity": "warning",
|
|
935
|
+
"kind": "boolean",
|
|
936
|
+
"summary": "A sitemap must not list URLs that ask not to be indexed",
|
|
937
|
+
"rigor": "vendor-spec",
|
|
938
|
+
"sources": [
|
|
939
|
+
"sitemaps-protocol",
|
|
940
|
+
"google-robots-meta"
|
|
941
|
+
]
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
"id": "sitemap.entry.non-canonical",
|
|
945
|
+
"scope": "site",
|
|
946
|
+
"severity": "warning",
|
|
947
|
+
"kind": "boolean",
|
|
948
|
+
"summary": "A sitemap should list canonical URLs",
|
|
949
|
+
"rigor": "vendor-spec",
|
|
950
|
+
"sources": [
|
|
951
|
+
"sitemaps-protocol",
|
|
952
|
+
"google-canonicalization"
|
|
953
|
+
]
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
"id": "sitemap.entry.protocol-mismatch",
|
|
957
|
+
"scope": "site",
|
|
958
|
+
"severity": "warning",
|
|
959
|
+
"kind": "boolean",
|
|
960
|
+
"summary": "A sitemap should not mix `http` and `https` entries",
|
|
961
|
+
"rigor": "spec-required",
|
|
962
|
+
"sources": [
|
|
963
|
+
"sitemaps-protocol"
|
|
964
|
+
]
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
"id": "sitemap.entry.redirects",
|
|
968
|
+
"scope": "site",
|
|
969
|
+
"severity": "warning",
|
|
970
|
+
"kind": "boolean",
|
|
971
|
+
"summary": "A sitemap should list final URLs, not URLs that redirect",
|
|
972
|
+
"rigor": "guideline",
|
|
973
|
+
"sources": [
|
|
974
|
+
"sitemaps-protocol",
|
|
975
|
+
"google-sitemaps"
|
|
976
|
+
]
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"id": "sitemap.entry.unreachable",
|
|
980
|
+
"scope": "site",
|
|
981
|
+
"severity": "error",
|
|
982
|
+
"kind": "boolean",
|
|
983
|
+
"summary": "Every URL a sitemap lists must answer",
|
|
984
|
+
"rigor": "vendor-spec",
|
|
985
|
+
"sources": [
|
|
986
|
+
"sitemaps-protocol",
|
|
987
|
+
"google-sitemaps"
|
|
988
|
+
]
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
"id": "sitemap.field.invalid",
|
|
992
|
+
"scope": "site",
|
|
993
|
+
"severity": "warning",
|
|
994
|
+
"kind": "boolean",
|
|
995
|
+
"summary": "`<changefreq>` and `<priority>` must hold the values the protocol defines",
|
|
996
|
+
"rigor": "spec-required",
|
|
997
|
+
"sources": [
|
|
998
|
+
"sitemaps-protocol",
|
|
999
|
+
"google-sitemaps"
|
|
1000
|
+
]
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
"id": "sitemap.index.child-error",
|
|
1004
|
+
"scope": "site",
|
|
1005
|
+
"severity": "error",
|
|
1006
|
+
"kind": "boolean",
|
|
1007
|
+
"summary": "Every child of a sitemap index must be reachable and parseable",
|
|
1008
|
+
"rigor": "spec-required",
|
|
1009
|
+
"sources": [
|
|
1010
|
+
"sitemaps-protocol"
|
|
1011
|
+
]
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
"id": "sitemap.lastmod.invalid",
|
|
1015
|
+
"scope": "site",
|
|
1016
|
+
"severity": "warning",
|
|
1017
|
+
"kind": "boolean",
|
|
1018
|
+
"summary": "`<lastmod>` must be a W3C Datetime, and must not be in the future",
|
|
1019
|
+
"rigor": "spec-required",
|
|
1020
|
+
"sources": [
|
|
1021
|
+
"sitemaps-protocol",
|
|
1022
|
+
"google-sitemaps"
|
|
1023
|
+
]
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"id": "sitemap.missing",
|
|
1027
|
+
"scope": "site",
|
|
1028
|
+
"severity": "warning",
|
|
1029
|
+
"kind": "boolean",
|
|
1030
|
+
"summary": "A site should publish a sitemap",
|
|
1031
|
+
"rigor": "guideline",
|
|
1032
|
+
"sources": [
|
|
1033
|
+
"sitemaps-protocol",
|
|
1034
|
+
"google-sitemaps"
|
|
1035
|
+
]
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
"id": "sitemap.orphans",
|
|
1039
|
+
"scope": "site",
|
|
1040
|
+
"severity": "warning",
|
|
1041
|
+
"kind": "boolean",
|
|
1042
|
+
"summary": "Indexable pages the crawl found should be listed in the sitemap",
|
|
1043
|
+
"rigor": "guideline",
|
|
1044
|
+
"sources": [
|
|
1045
|
+
"sitemaps-protocol",
|
|
1046
|
+
"google-sitemaps"
|
|
1047
|
+
]
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
"id": "sitemap.unparsable",
|
|
1051
|
+
"scope": "site",
|
|
1052
|
+
"severity": "error",
|
|
1053
|
+
"kind": "boolean",
|
|
1054
|
+
"summary": "A located sitemap must be a well-formed `<urlset>` or `<sitemapindex>`",
|
|
1055
|
+
"rigor": "spec-required",
|
|
1056
|
+
"sources": [
|
|
1057
|
+
"sitemaps-protocol"
|
|
1058
|
+
]
|
|
1059
|
+
},
|
|
411
1060
|
{
|
|
412
1061
|
"id": "title.descriptive",
|
|
413
1062
|
"scope": "prose",
|