@goflag/cli 0.2.5 → 0.2.8
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 +99 -14
- package/dist/{chunk-5FO2Q3II.js → chunk-SKX7UCGC.js} +318 -25
- package/dist/chunk-SKX7UCGC.js.map +1 -0
- package/dist/cli.js +1018 -217
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +64 -4
- package/dist/index.js +1 -1
- package/flags.json +302 -0
- package/package.json +7 -2
- package/rules.json +517 -0
- package/dist/chunk-5FO2Q3II.js.map +0 -1
package/rules.json
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
{
|
|
2
|
+
"counts": {
|
|
3
|
+
"page": 11,
|
|
4
|
+
"site": 3,
|
|
5
|
+
"prose": 4
|
|
6
|
+
},
|
|
7
|
+
"sources": {
|
|
8
|
+
"bing-webmaster-guidelines": {
|
|
9
|
+
"id": "bing-webmaster-guidelines",
|
|
10
|
+
"publisher": "Microsoft",
|
|
11
|
+
"title": "Bing Webmaster Guidelines",
|
|
12
|
+
"rigor": "guideline",
|
|
13
|
+
"url": "https://www.bing.com/webmasters/help/webmasters-guidelines-30fba23a"
|
|
14
|
+
},
|
|
15
|
+
"google-canonicalization": {
|
|
16
|
+
"id": "google-canonicalization",
|
|
17
|
+
"publisher": "Google",
|
|
18
|
+
"title": "Consolidate duplicate URLs (canonicalization)",
|
|
19
|
+
"rigor": "vendor-spec",
|
|
20
|
+
"url": "https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls"
|
|
21
|
+
},
|
|
22
|
+
"google-robots-meta": {
|
|
23
|
+
"id": "google-robots-meta",
|
|
24
|
+
"publisher": "Google",
|
|
25
|
+
"title": "Robots meta tag and X-Robots-Tag",
|
|
26
|
+
"rigor": "vendor-spec",
|
|
27
|
+
"url": "https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag"
|
|
28
|
+
},
|
|
29
|
+
"google-snippet": {
|
|
30
|
+
"id": "google-snippet",
|
|
31
|
+
"publisher": "Google",
|
|
32
|
+
"title": "Control your snippets in search results",
|
|
33
|
+
"rigor": "guideline",
|
|
34
|
+
"url": "https://developers.google.com/search/docs/appearance/snippet"
|
|
35
|
+
},
|
|
36
|
+
"google-title-link": {
|
|
37
|
+
"id": "google-title-link",
|
|
38
|
+
"publisher": "Google",
|
|
39
|
+
"title": "Influence your title links in search results",
|
|
40
|
+
"rigor": "guideline",
|
|
41
|
+
"url": "https://developers.google.com/search/docs/appearance/title-link"
|
|
42
|
+
},
|
|
43
|
+
"ietf-rfc6596": {
|
|
44
|
+
"id": "ietf-rfc6596",
|
|
45
|
+
"publisher": "IETF",
|
|
46
|
+
"title": "RFC 6596 — The Canonical Link Relation",
|
|
47
|
+
"rigor": "normative",
|
|
48
|
+
"url": "https://www.rfc-editor.org/rfc/rfc6596"
|
|
49
|
+
},
|
|
50
|
+
"mdn-viewport": {
|
|
51
|
+
"id": "mdn-viewport",
|
|
52
|
+
"publisher": "MDN / Mozilla",
|
|
53
|
+
"title": "MDN — Viewport meta tag",
|
|
54
|
+
"rigor": "guideline",
|
|
55
|
+
"url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag"
|
|
56
|
+
},
|
|
57
|
+
"meta-og-sharing": {
|
|
58
|
+
"id": "meta-og-sharing",
|
|
59
|
+
"publisher": "Meta",
|
|
60
|
+
"title": "Sharing — webmasters (Open Graph usage)",
|
|
61
|
+
"rigor": "vendor-spec",
|
|
62
|
+
"url": "https://developers.facebook.com/docs/sharing/webmasters/"
|
|
63
|
+
},
|
|
64
|
+
"moz-meta-description": {
|
|
65
|
+
"id": "moz-meta-description",
|
|
66
|
+
"publisher": "Moz",
|
|
67
|
+
"title": "Meta description best practices",
|
|
68
|
+
"rigor": "heuristic",
|
|
69
|
+
"url": "https://moz.com/learn/seo/meta-description"
|
|
70
|
+
},
|
|
71
|
+
"moz-title-tag": {
|
|
72
|
+
"id": "moz-title-tag",
|
|
73
|
+
"publisher": "Moz",
|
|
74
|
+
"title": "Title tag best practices",
|
|
75
|
+
"rigor": "heuristic",
|
|
76
|
+
"url": "https://moz.com/learn/seo/title-tag"
|
|
77
|
+
},
|
|
78
|
+
"ogp": {
|
|
79
|
+
"id": "ogp",
|
|
80
|
+
"publisher": "ogp.me",
|
|
81
|
+
"title": "The Open Graph protocol",
|
|
82
|
+
"rigor": "vendor-spec",
|
|
83
|
+
"url": "https://ogp.me/"
|
|
84
|
+
},
|
|
85
|
+
"w3c-i18n-language-tags": {
|
|
86
|
+
"id": "w3c-i18n-language-tags",
|
|
87
|
+
"publisher": "W3C",
|
|
88
|
+
"title": "W3C i18n — Language tags in HTML and XML",
|
|
89
|
+
"rigor": "guideline",
|
|
90
|
+
"url": "https://www.w3.org/International/articles/language-tags/"
|
|
91
|
+
},
|
|
92
|
+
"whatwg-html-lang": {
|
|
93
|
+
"id": "whatwg-html-lang",
|
|
94
|
+
"publisher": "WHATWG",
|
|
95
|
+
"title": "HTML Living Standard — the lang and xml:lang attributes",
|
|
96
|
+
"rigor": "normative",
|
|
97
|
+
"url": "https://html.spec.whatwg.org/multipage/dom.html"
|
|
98
|
+
},
|
|
99
|
+
"whatwg-html-standard-metadata-names": {
|
|
100
|
+
"id": "whatwg-html-standard-metadata-names",
|
|
101
|
+
"publisher": "WHATWG",
|
|
102
|
+
"title": "HTML Living Standard — standard metadata names",
|
|
103
|
+
"rigor": "normative",
|
|
104
|
+
"url": "https://html.spec.whatwg.org/multipage/semantics.html"
|
|
105
|
+
},
|
|
106
|
+
"whatwg-html-title": {
|
|
107
|
+
"id": "whatwg-html-title",
|
|
108
|
+
"publisher": "WHATWG",
|
|
109
|
+
"title": "HTML Living Standard — the title element",
|
|
110
|
+
"rigor": "normative",
|
|
111
|
+
"url": "https://html.spec.whatwg.org/multipage/semantics.html"
|
|
112
|
+
},
|
|
113
|
+
"whatwg-url": {
|
|
114
|
+
"id": "whatwg-url",
|
|
115
|
+
"publisher": "WHATWG",
|
|
116
|
+
"title": "URL Standard",
|
|
117
|
+
"rigor": "normative",
|
|
118
|
+
"url": "https://url.spec.whatwg.org/"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"rules": [
|
|
122
|
+
{
|
|
123
|
+
"id": "canonical.absolute",
|
|
124
|
+
"scope": "page",
|
|
125
|
+
"category": "canonical",
|
|
126
|
+
"severity": "error",
|
|
127
|
+
"kind": "boolean",
|
|
128
|
+
"summary": "`rel=\"canonical\"` must be an absolute, fully-qualified URL",
|
|
129
|
+
"why": "Consumers read the literal attribute value, not the browser-resolved URL. A relative canonical means every consumer resolves it against whatever base it assumes — including the wrong origin.",
|
|
130
|
+
"rigor": "vendor-spec",
|
|
131
|
+
"sources": [
|
|
132
|
+
"ietf-rfc6596",
|
|
133
|
+
"google-canonicalization",
|
|
134
|
+
"whatwg-url"
|
|
135
|
+
],
|
|
136
|
+
"reads": [
|
|
137
|
+
"meta.canonical"
|
|
138
|
+
],
|
|
139
|
+
"relates": [
|
|
140
|
+
"canonical.missing"
|
|
141
|
+
],
|
|
142
|
+
"expected": "an absolute `http(s)://` canonical URL"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "canonical.missing",
|
|
146
|
+
"scope": "page",
|
|
147
|
+
"category": "canonical",
|
|
148
|
+
"severity": "warning",
|
|
149
|
+
"kind": "boolean",
|
|
150
|
+
"summary": "Declare a `<link rel=\"canonical\">` so search engines pick the right URL",
|
|
151
|
+
"why": "Without a canonical, engines choose among duplicate URLs (tracking params, http/https, trailing slashes) themselves — and may split ranking signals across them.",
|
|
152
|
+
"rigor": "vendor-spec",
|
|
153
|
+
"sources": [
|
|
154
|
+
"ietf-rfc6596",
|
|
155
|
+
"google-canonicalization"
|
|
156
|
+
],
|
|
157
|
+
"reads": [
|
|
158
|
+
"meta.canonical"
|
|
159
|
+
],
|
|
160
|
+
"relates": [
|
|
161
|
+
"canonical.absolute"
|
|
162
|
+
],
|
|
163
|
+
"expected": "a `<link rel=\"canonical\">` declaration",
|
|
164
|
+
"fix": {
|
|
165
|
+
"title": "Declare the canonical URL",
|
|
166
|
+
"snippet": "// app/…/page.tsx — relative canonicals resolve against metadataBase,\n// which defaults to localhost and silently breaks in production.\nexport const metadata = {\n metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL!),\n alternates: { canonical: \"/the-page\" },\n};",
|
|
167
|
+
"language": "tsx"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"id": "description.accurate",
|
|
172
|
+
"scope": "prose",
|
|
173
|
+
"category": "meta",
|
|
174
|
+
"severity": null,
|
|
175
|
+
"kind": "prose",
|
|
176
|
+
"summary": "The meta description summarizes this page truthfully",
|
|
177
|
+
"why": "The description is the one sentence you get to write in a search result. A boilerplate line repeated site-wide, or one that promises something the page does not deliver, gets replaced by improvised page text — or gets the click and loses the visitor.",
|
|
178
|
+
"rigor": "guideline",
|
|
179
|
+
"sources": [
|
|
180
|
+
"google-snippet",
|
|
181
|
+
"moz-meta-description"
|
|
182
|
+
],
|
|
183
|
+
"reads": [
|
|
184
|
+
"meta.description",
|
|
185
|
+
"document.title",
|
|
186
|
+
"http.finalUrl"
|
|
187
|
+
],
|
|
188
|
+
"prose": "Does the description accurately summarize this page's content, and is it written for this page rather than copied across the site?"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"id": "description.length",
|
|
192
|
+
"scope": "page",
|
|
193
|
+
"category": "meta",
|
|
194
|
+
"severity": "warning",
|
|
195
|
+
"kind": "scored",
|
|
196
|
+
"summary": "Keep descriptions between 50 and 160 characters",
|
|
197
|
+
"why": "Snippets truncate around 160 characters and very short descriptions get replaced by improvised page text. Display folklore, not spec.",
|
|
198
|
+
"rigor": "heuristic",
|
|
199
|
+
"sources": [
|
|
200
|
+
"google-snippet",
|
|
201
|
+
"moz-meta-description"
|
|
202
|
+
],
|
|
203
|
+
"reads": [
|
|
204
|
+
"meta.description"
|
|
205
|
+
],
|
|
206
|
+
"relates": [
|
|
207
|
+
"description.missing"
|
|
208
|
+
],
|
|
209
|
+
"expected": "50–160 characters",
|
|
210
|
+
"bands": {
|
|
211
|
+
"ideal": [
|
|
212
|
+
50,
|
|
213
|
+
160
|
|
214
|
+
],
|
|
215
|
+
"acceptable": [
|
|
216
|
+
25,
|
|
217
|
+
200
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
"severityByBand": {
|
|
221
|
+
"acceptable": "warning",
|
|
222
|
+
"poor": "warning"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": "description.missing",
|
|
227
|
+
"scope": "page",
|
|
228
|
+
"category": "meta",
|
|
229
|
+
"severity": "warning",
|
|
230
|
+
"kind": "boolean",
|
|
231
|
+
"summary": "Provide a `<meta name=\"description\">` on every indexable page",
|
|
232
|
+
"why": "The description feeds the search-result snippet. Without one, engines improvise from page text — usually worse than a sentence you wrote.",
|
|
233
|
+
"rigor": "spec-recommended",
|
|
234
|
+
"sources": [
|
|
235
|
+
"whatwg-html-standard-metadata-names",
|
|
236
|
+
"google-snippet"
|
|
237
|
+
],
|
|
238
|
+
"reads": [
|
|
239
|
+
"meta.description"
|
|
240
|
+
],
|
|
241
|
+
"relates": [
|
|
242
|
+
"description.length",
|
|
243
|
+
"og.description.missing"
|
|
244
|
+
],
|
|
245
|
+
"expected": "a non-empty `<meta name=\"description\">`",
|
|
246
|
+
"fix": {
|
|
247
|
+
"title": "Add a meta description",
|
|
248
|
+
"snippet": "// app/…/page.tsx\nexport const metadata = {\n description: \"One sentence that promises what this page delivers.\",\n};",
|
|
249
|
+
"language": "tsx"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "hreflang.missing",
|
|
254
|
+
"scope": "site",
|
|
255
|
+
"severity": "error",
|
|
256
|
+
"kind": "boolean",
|
|
257
|
+
"summary": "Pages on a multilingual site must advertise their locale alternates",
|
|
258
|
+
"rigor": null,
|
|
259
|
+
"sources": []
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": "hreflang.sitemap-mismatch",
|
|
263
|
+
"scope": "site",
|
|
264
|
+
"severity": "warning",
|
|
265
|
+
"kind": "boolean",
|
|
266
|
+
"summary": "`<head>` alternates and sitemap locale coverage must agree",
|
|
267
|
+
"rigor": null,
|
|
268
|
+
"sources": []
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"id": "lang.matches-content",
|
|
272
|
+
"scope": "prose",
|
|
273
|
+
"category": "i18n",
|
|
274
|
+
"severity": null,
|
|
275
|
+
"kind": "prose",
|
|
276
|
+
"summary": "The declared `lang` is the language the page is actually written in",
|
|
277
|
+
"why": "A wrong `lang` is worse than a missing one: screen readers switch to the wrong pronunciation rules, browsers offer to translate a page that is already in the reader's language, and search engines file the page under the wrong audience. Nothing in the markup contradicts it, so no mechanical check can catch it.",
|
|
278
|
+
"rigor": "spec-required",
|
|
279
|
+
"sources": [
|
|
280
|
+
"whatwg-html-lang",
|
|
281
|
+
"w3c-i18n-language-tags"
|
|
282
|
+
],
|
|
283
|
+
"reads": [
|
|
284
|
+
"document.lang",
|
|
285
|
+
"document.title",
|
|
286
|
+
"meta.description",
|
|
287
|
+
"openGraph.locale"
|
|
288
|
+
],
|
|
289
|
+
"prose": "Is the text of this page actually written in the language its `lang` attribute declares?"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"id": "og.description.missing",
|
|
293
|
+
"scope": "page",
|
|
294
|
+
"category": "opengraph",
|
|
295
|
+
"severity": "info",
|
|
296
|
+
"kind": "boolean",
|
|
297
|
+
"summary": "Set an explicit `og:description` for richer link unfurls",
|
|
298
|
+
"why": "A page that opted into Open Graph but omitted og:description gets unfurls that fall back to the meta description — or to nothing.",
|
|
299
|
+
"rigor": "vendor-spec",
|
|
300
|
+
"sources": [
|
|
301
|
+
"ogp",
|
|
302
|
+
"meta-og-sharing"
|
|
303
|
+
],
|
|
304
|
+
"reads": [
|
|
305
|
+
"openGraph.description",
|
|
306
|
+
"openGraph.title",
|
|
307
|
+
"openGraph.images",
|
|
308
|
+
"openGraph.url"
|
|
309
|
+
],
|
|
310
|
+
"relates": [
|
|
311
|
+
"description.missing"
|
|
312
|
+
],
|
|
313
|
+
"expected": "an `og:description` alongside the other `og:*` tags"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"id": "og.image.missing",
|
|
317
|
+
"scope": "page",
|
|
318
|
+
"category": "opengraph",
|
|
319
|
+
"severity": "warning",
|
|
320
|
+
"kind": "boolean",
|
|
321
|
+
"summary": "Provide at least one `og:image` so links unfurl with a preview",
|
|
322
|
+
"why": "og:image is one of the four required Open Graph properties, and the one with the most visible payoff: without it, unfurls are text-only or pick a random body image.",
|
|
323
|
+
"rigor": "vendor-spec",
|
|
324
|
+
"sources": [
|
|
325
|
+
"ogp",
|
|
326
|
+
"meta-og-sharing"
|
|
327
|
+
],
|
|
328
|
+
"reads": [
|
|
329
|
+
"openGraph.images"
|
|
330
|
+
],
|
|
331
|
+
"relates": [
|
|
332
|
+
"og.title.missing"
|
|
333
|
+
],
|
|
334
|
+
"expected": "at least one `og:image`",
|
|
335
|
+
"fix": {
|
|
336
|
+
"title": "Add an og:image",
|
|
337
|
+
"snippet": "// app/…/page.tsx\nexport const metadata = {\n openGraph: { images: [{ url: \"/og.png\", width: 1200, height: 630 }] },\n};\n// Or generate one per page: app/…/opengraph-image.tsx with ImageResponse.",
|
|
338
|
+
"language": "tsx"
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"id": "og.image.representative",
|
|
343
|
+
"scope": "prose",
|
|
344
|
+
"category": "opengraph",
|
|
345
|
+
"severity": null,
|
|
346
|
+
"kind": "prose",
|
|
347
|
+
"summary": "The `og:image` represents this page and survives the unfurl crop",
|
|
348
|
+
"why": "The preview image is the whole payload of a shared link. A generic site-wide banner, or artwork whose subject sits outside the ~1.91:1 crop, is present enough to pass every mechanical check and still communicate nothing.",
|
|
349
|
+
"rigor": "guideline",
|
|
350
|
+
"sources": [
|
|
351
|
+
"ogp",
|
|
352
|
+
"meta-og-sharing"
|
|
353
|
+
],
|
|
354
|
+
"reads": [
|
|
355
|
+
"openGraph.images",
|
|
356
|
+
"openGraph.title",
|
|
357
|
+
"document.title",
|
|
358
|
+
"http.finalUrl"
|
|
359
|
+
],
|
|
360
|
+
"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
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"id": "og.title.missing",
|
|
364
|
+
"scope": "page",
|
|
365
|
+
"category": "opengraph",
|
|
366
|
+
"severity": "warning",
|
|
367
|
+
"kind": "boolean",
|
|
368
|
+
"summary": "Set an explicit `og:title` instead of relying on `<title>` fallback",
|
|
369
|
+
"why": "og:title is one of the four properties the Open Graph protocol requires. Consumers that fall back to `<title>` drag site-name boilerplate into the unfurl.",
|
|
370
|
+
"rigor": "vendor-spec",
|
|
371
|
+
"sources": [
|
|
372
|
+
"ogp"
|
|
373
|
+
],
|
|
374
|
+
"reads": [
|
|
375
|
+
"openGraph.title"
|
|
376
|
+
],
|
|
377
|
+
"relates": [
|
|
378
|
+
"title.missing",
|
|
379
|
+
"og.image.missing"
|
|
380
|
+
],
|
|
381
|
+
"expected": "an explicit `og:title`"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"id": "robots.blocks-site",
|
|
385
|
+
"scope": "site",
|
|
386
|
+
"severity": "error",
|
|
387
|
+
"kind": "boolean",
|
|
388
|
+
"summary": "`robots.txt` must not forbid crawling a site that asks to be indexed",
|
|
389
|
+
"rigor": null,
|
|
390
|
+
"sources": []
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"id": "robots.conflict",
|
|
394
|
+
"scope": "page",
|
|
395
|
+
"category": "robots",
|
|
396
|
+
"severity": "error",
|
|
397
|
+
"kind": "boolean",
|
|
398
|
+
"summary": "`robots`, `googlebot`, and `X-Robots-Tag` must not contradict each other",
|
|
399
|
+
"why": "When directives conflict, crawlers resolve them with the most restrictive interpretation — a stray `noindex` wins over the `index` you meant, and the page silently drops out of search.",
|
|
400
|
+
"rigor": "vendor-spec",
|
|
401
|
+
"sources": [
|
|
402
|
+
"google-robots-meta"
|
|
403
|
+
],
|
|
404
|
+
"reads": [
|
|
405
|
+
"meta.robots",
|
|
406
|
+
"meta.googlebot",
|
|
407
|
+
"http.headers"
|
|
408
|
+
],
|
|
409
|
+
"expected": "consistent indexing directives across meta tags and headers"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"id": "title.descriptive",
|
|
413
|
+
"scope": "prose",
|
|
414
|
+
"category": "document",
|
|
415
|
+
"severity": null,
|
|
416
|
+
"kind": "prose",
|
|
417
|
+
"summary": "The `<title>` describes this specific page",
|
|
418
|
+
"why": "A title that repeats the site name, or describes the section rather than the page, gives a searcher no way to tell two results apart — and gives Google a reason to rewrite it into something you did not choose.",
|
|
419
|
+
"rigor": "guideline",
|
|
420
|
+
"sources": [
|
|
421
|
+
"google-title-link",
|
|
422
|
+
"bing-webmaster-guidelines"
|
|
423
|
+
],
|
|
424
|
+
"reads": [
|
|
425
|
+
"document.title",
|
|
426
|
+
"meta.description",
|
|
427
|
+
"openGraph.title",
|
|
428
|
+
"http.finalUrl"
|
|
429
|
+
],
|
|
430
|
+
"prose": "Does the title describe what is on THIS page specifically — not the site, not the section — and would it be distinguishable from the other pages of this site in a list of search results?"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"id": "title.length",
|
|
434
|
+
"scope": "page",
|
|
435
|
+
"category": "document",
|
|
436
|
+
"severity": "warning",
|
|
437
|
+
"kind": "scored",
|
|
438
|
+
"summary": "Keep `<title>` between 10 and 60 characters",
|
|
439
|
+
"why": "Long titles get truncated in search results and short ones waste the one line every result gets. The window is display folklore, not spec — Google states title length is not a ranking factor.",
|
|
440
|
+
"rigor": "heuristic",
|
|
441
|
+
"sources": [
|
|
442
|
+
"google-title-link",
|
|
443
|
+
"moz-title-tag"
|
|
444
|
+
],
|
|
445
|
+
"reads": [
|
|
446
|
+
"document.title"
|
|
447
|
+
],
|
|
448
|
+
"relates": [
|
|
449
|
+
"title.missing"
|
|
450
|
+
],
|
|
451
|
+
"expected": "10–60 characters",
|
|
452
|
+
"bands": {
|
|
453
|
+
"ideal": [
|
|
454
|
+
10,
|
|
455
|
+
60
|
|
456
|
+
],
|
|
457
|
+
"acceptable": [
|
|
458
|
+
5,
|
|
459
|
+
70
|
|
460
|
+
]
|
|
461
|
+
},
|
|
462
|
+
"severityByBand": {
|
|
463
|
+
"acceptable": "warning",
|
|
464
|
+
"poor": "warning"
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"id": "title.missing",
|
|
469
|
+
"scope": "page",
|
|
470
|
+
"category": "document",
|
|
471
|
+
"severity": "error",
|
|
472
|
+
"kind": "boolean",
|
|
473
|
+
"summary": "Every page needs a non-empty `<title>`",
|
|
474
|
+
"why": "The title element is required by the HTML standard and is the primary label search engines, browser tabs, bookmarks, and screen readers use for the page.",
|
|
475
|
+
"rigor": "spec-required",
|
|
476
|
+
"sources": [
|
|
477
|
+
"whatwg-html-title"
|
|
478
|
+
],
|
|
479
|
+
"reads": [
|
|
480
|
+
"document.title"
|
|
481
|
+
],
|
|
482
|
+
"relates": [
|
|
483
|
+
"title.length",
|
|
484
|
+
"og.title.missing"
|
|
485
|
+
],
|
|
486
|
+
"expected": "a non-empty `<title>` element",
|
|
487
|
+
"fix": {
|
|
488
|
+
"title": "Add a <title> to <head>",
|
|
489
|
+
"snippet": "// app/…/page.tsx — App Router owns the <head>; never hand-write the tag.\nexport const metadata = { title: \"Page name — Site name\" };",
|
|
490
|
+
"language": "tsx"
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"id": "viewport.missing",
|
|
495
|
+
"scope": "page",
|
|
496
|
+
"category": "meta",
|
|
497
|
+
"severity": "warning",
|
|
498
|
+
"kind": "boolean",
|
|
499
|
+
"summary": "Declare a `<meta name=\"viewport\">` so mobile browsers render at the right scale",
|
|
500
|
+
"why": "Without a viewport declaration mobile browsers render at desktop width and scale down — unreadable text and a mobile-usability demotion.",
|
|
501
|
+
"rigor": "guideline",
|
|
502
|
+
"sources": [
|
|
503
|
+
"mdn-viewport",
|
|
504
|
+
"whatwg-html-standard-metadata-names"
|
|
505
|
+
],
|
|
506
|
+
"reads": [
|
|
507
|
+
"meta.viewport"
|
|
508
|
+
],
|
|
509
|
+
"expected": "a `<meta name=\"viewport\">` declaration",
|
|
510
|
+
"fix": {
|
|
511
|
+
"title": "Add a viewport meta",
|
|
512
|
+
"snippet": "// app/layout.tsx — a dedicated export, not part of `metadata`.\nexport const viewport = { width: \"device-width\", initialScale: 1 };",
|
|
513
|
+
"language": "tsx"
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
]
|
|
517
|
+
}
|