search-auditor 0.1.0 → 0.2.0

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.
Files changed (225) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +87 -38
  3. package/dist/cli.js +17 -9
  4. package/dist/collect.d.ts +5 -1
  5. package/dist/collect.js +186 -11
  6. package/dist/i18n/en.d.ts +462 -7
  7. package/dist/i18n/en.js +480 -25
  8. package/dist/i18n/index.d.ts +924 -14
  9. package/dist/i18n/index.js +1 -1
  10. package/dist/i18n/ru.js +484 -24
  11. package/dist/index.d.ts +3 -145
  12. package/dist/index.js +14 -53
  13. package/dist/rules/can/can-1.d.ts +2 -0
  14. package/dist/rules/can/can-1.js +14 -0
  15. package/dist/rules/can/can-2.d.ts +2 -0
  16. package/dist/rules/can/can-2.js +20 -0
  17. package/dist/rules/can/can-3.d.ts +2 -0
  18. package/dist/rules/can/can-3.js +32 -0
  19. package/dist/rules/can/can-4.d.ts +2 -0
  20. package/dist/rules/can/can-4.js +18 -0
  21. package/dist/rules/can/can-5.d.ts +2 -0
  22. package/dist/rules/can/can-5.js +17 -0
  23. package/dist/rules/can/can-6.d.ts +2 -0
  24. package/dist/rules/can/can-6.js +22 -0
  25. package/dist/rules/can/can-7.d.ts +2 -0
  26. package/dist/rules/can/can-7.js +32 -0
  27. package/dist/rules/can/can-8.d.ts +2 -0
  28. package/dist/rules/can/can-8.js +23 -0
  29. package/dist/rules/can/can-9.d.ts +2 -0
  30. package/dist/rules/can/can-9.js +24 -0
  31. package/dist/rules/can/index.d.ts +7 -0
  32. package/dist/rules/can/index.js +10 -0
  33. package/dist/rules/can/shared.d.ts +11 -0
  34. package/dist/rules/can/shared.js +26 -0
  35. package/dist/rules/data/data-1.d.ts +2 -0
  36. package/dist/rules/data/data-1.js +16 -0
  37. package/dist/rules/data/data-2.d.ts +2 -0
  38. package/dist/rules/data/data-2.js +25 -0
  39. package/dist/rules/data/data-3.d.ts +2 -0
  40. package/dist/rules/data/data-3.js +29 -0
  41. package/dist/rules/data/data-4.d.ts +2 -0
  42. package/dist/rules/data/data-4.js +17 -0
  43. package/dist/rules/data/data-5.d.ts +2 -0
  44. package/dist/rules/data/data-5.js +28 -0
  45. package/dist/rules/data/data-6.d.ts +2 -0
  46. package/dist/rules/data/data-6.js +16 -0
  47. package/dist/rules/data/index.d.ts +8 -0
  48. package/dist/rules/data/index.js +7 -0
  49. package/dist/rules/data/shared.d.ts +22 -0
  50. package/dist/rules/data/shared.js +59 -0
  51. package/dist/rules/idx/idx-1.d.ts +2 -0
  52. package/dist/rules/idx/idx-1.js +15 -0
  53. package/dist/rules/idx/idx-2.d.ts +2 -0
  54. package/dist/rules/idx/idx-2.js +22 -0
  55. package/dist/rules/idx/idx-3.d.ts +2 -0
  56. package/dist/rules/idx/idx-3.js +35 -0
  57. package/dist/rules/idx/idx-4.d.ts +2 -0
  58. package/dist/rules/idx/idx-4.js +40 -0
  59. package/dist/rules/idx/idx-5.d.ts +2 -0
  60. package/dist/rules/idx/idx-5.js +24 -0
  61. package/dist/rules/idx/idx-6.d.ts +2 -0
  62. package/dist/rules/idx/idx-6.js +25 -0
  63. package/dist/rules/idx/idx-7.d.ts +2 -0
  64. package/dist/rules/idx/idx-7.js +29 -0
  65. package/dist/rules/idx/idx-8.d.ts +2 -0
  66. package/dist/rules/idx/idx-8.js +25 -0
  67. package/dist/rules/idx/idx-9.d.ts +2 -0
  68. package/dist/rules/idx/idx-9.js +22 -0
  69. package/dist/rules/idx/index.d.ts +7 -0
  70. package/dist/rules/idx/index.js +10 -0
  71. package/dist/rules/idx/shared.d.ts +18 -0
  72. package/dist/rules/idx/shared.js +18 -0
  73. package/dist/rules/img/img-1.d.ts +2 -0
  74. package/dist/rules/img/img-1.js +18 -0
  75. package/dist/rules/img/img-2.d.ts +2 -0
  76. package/dist/rules/img/img-2.js +20 -0
  77. package/dist/rules/img/img-3.d.ts +2 -0
  78. package/dist/rules/img/img-3.js +24 -0
  79. package/dist/rules/img/img-4.d.ts +2 -0
  80. package/dist/rules/img/img-4.js +19 -0
  81. package/dist/rules/img/img-5.d.ts +2 -0
  82. package/dist/rules/img/img-5.js +18 -0
  83. package/dist/rules/img/img-6.d.ts +2 -0
  84. package/dist/rules/img/img-6.js +20 -0
  85. package/dist/rules/img/img-7.d.ts +2 -0
  86. package/dist/rules/img/img-7.js +22 -0
  87. package/dist/rules/img/index.d.ts +7 -0
  88. package/dist/rules/img/index.js +8 -0
  89. package/dist/rules/img/shared.d.ts +26 -0
  90. package/dist/rules/img/shared.js +38 -0
  91. package/dist/rules/index.d.ts +1 -1
  92. package/dist/rules/index.js +23 -6
  93. package/dist/rules/lang/index.d.ts +8 -0
  94. package/dist/rules/lang/index.js +9 -0
  95. package/dist/rules/lang/lang-1.d.ts +2 -0
  96. package/dist/rules/lang/lang-1.js +17 -0
  97. package/dist/rules/lang/lang-2.d.ts +2 -0
  98. package/dist/rules/lang/lang-2.js +30 -0
  99. package/dist/rules/lang/lang-3.d.ts +2 -0
  100. package/dist/rules/lang/lang-3.js +22 -0
  101. package/dist/rules/lang/lang-4.d.ts +2 -0
  102. package/dist/rules/lang/lang-4.js +23 -0
  103. package/dist/rules/lang/lang-5.d.ts +2 -0
  104. package/dist/rules/lang/lang-5.js +33 -0
  105. package/dist/rules/lang/lang-6.d.ts +2 -0
  106. package/dist/rules/lang/lang-6.js +25 -0
  107. package/dist/rules/lang/lang-7.d.ts +2 -0
  108. package/dist/rules/lang/lang-7.js +25 -0
  109. package/dist/rules/lang/lang-8.d.ts +2 -0
  110. package/dist/rules/lang/lang-8.js +19 -0
  111. package/dist/rules/lang/shared.d.ts +7 -0
  112. package/dist/rules/lang/shared.js +5 -0
  113. package/dist/rules/lnk/index.d.ts +8 -0
  114. package/dist/rules/lnk/index.js +8 -0
  115. package/dist/rules/lnk/lnk-1.d.ts +2 -0
  116. package/dist/rules/lnk/lnk-1.js +22 -0
  117. package/dist/rules/lnk/lnk-2.d.ts +2 -0
  118. package/dist/rules/lnk/lnk-2.js +18 -0
  119. package/dist/rules/lnk/lnk-3.d.ts +2 -0
  120. package/dist/rules/lnk/lnk-3.js +32 -0
  121. package/dist/rules/lnk/lnk-4.d.ts +2 -0
  122. package/dist/rules/lnk/lnk-4.js +18 -0
  123. package/dist/rules/lnk/lnk-5.d.ts +2 -0
  124. package/dist/rules/lnk/lnk-5.js +22 -0
  125. package/dist/rules/lnk/lnk-6.d.ts +2 -0
  126. package/dist/rules/lnk/lnk-6.js +24 -0
  127. package/dist/rules/lnk/lnk-7.d.ts +2 -0
  128. package/dist/rules/lnk/lnk-7.js +26 -0
  129. package/dist/rules/lnk/shared.d.ts +20 -0
  130. package/dist/rules/lnk/shared.js +29 -0
  131. package/dist/rules/map/index.d.ts +7 -0
  132. package/dist/rules/map/index.js +9 -0
  133. package/dist/rules/map/map-1.d.ts +2 -0
  134. package/dist/rules/map/map-1.js +15 -0
  135. package/dist/rules/map/map-2.d.ts +2 -0
  136. package/dist/rules/map/map-2.js +26 -0
  137. package/dist/rules/map/map-3.d.ts +2 -0
  138. package/dist/rules/map/map-3.js +24 -0
  139. package/dist/rules/map/map-4.d.ts +2 -0
  140. package/dist/rules/map/map-4.js +34 -0
  141. package/dist/rules/map/map-5.d.ts +2 -0
  142. package/dist/rules/map/map-5.js +28 -0
  143. package/dist/rules/map/map-6.d.ts +2 -0
  144. package/dist/rules/map/map-6.js +22 -0
  145. package/dist/rules/map/map-7.d.ts +2 -0
  146. package/dist/rules/map/map-7.js +18 -0
  147. package/dist/rules/map/map-8.d.ts +2 -0
  148. package/dist/rules/map/map-8.js +31 -0
  149. package/dist/rules/map/shared.d.ts +14 -0
  150. package/dist/rules/map/shared.js +12 -0
  151. package/dist/rules/meta/index.d.ts +8 -0
  152. package/dist/rules/meta/index.js +10 -0
  153. package/dist/rules/meta/meta-1.d.ts +2 -0
  154. package/dist/rules/meta/meta-1.js +20 -0
  155. package/dist/rules/meta/meta-2.d.ts +2 -0
  156. package/dist/rules/meta/meta-2.js +19 -0
  157. package/dist/rules/meta/meta-3.d.ts +2 -0
  158. package/dist/rules/meta/meta-3.js +26 -0
  159. package/dist/rules/meta/meta-4.d.ts +2 -0
  160. package/dist/rules/meta/meta-4.js +22 -0
  161. package/dist/rules/meta/meta-5.d.ts +2 -0
  162. package/dist/rules/meta/meta-5.js +17 -0
  163. package/dist/rules/meta/meta-6.d.ts +2 -0
  164. package/dist/rules/meta/meta-6.js +18 -0
  165. package/dist/rules/meta/meta-7.d.ts +2 -0
  166. package/dist/rules/meta/meta-7.js +18 -0
  167. package/dist/rules/meta/meta-8.d.ts +2 -0
  168. package/dist/rules/meta/meta-8.js +17 -0
  169. package/dist/rules/meta/meta-9.d.ts +2 -0
  170. package/dist/rules/meta/meta-9.js +16 -0
  171. package/dist/rules/meta/shared.d.ts +15 -0
  172. package/dist/rules/meta/shared.js +9 -0
  173. package/dist/rules/sec/index.d.ts +8 -0
  174. package/dist/rules/sec/index.js +8 -0
  175. package/dist/rules/sec/sec-1.d.ts +2 -0
  176. package/dist/rules/sec/sec-1.js +21 -0
  177. package/dist/rules/sec/sec-2.d.ts +2 -0
  178. package/dist/rules/sec/sec-2.js +17 -0
  179. package/dist/rules/sec/sec-3.d.ts +2 -0
  180. package/dist/rules/sec/sec-3.js +18 -0
  181. package/dist/rules/sec/sec-4.d.ts +2 -0
  182. package/dist/rules/sec/sec-4.js +23 -0
  183. package/dist/rules/sec/sec-5.d.ts +2 -0
  184. package/dist/rules/sec/sec-5.js +23 -0
  185. package/dist/rules/sec/sec-6.d.ts +2 -0
  186. package/dist/rules/sec/sec-6.js +32 -0
  187. package/dist/rules/sec/sec-7.d.ts +2 -0
  188. package/dist/rules/sec/sec-7.js +26 -0
  189. package/dist/rules/sec/shared.d.ts +10 -0
  190. package/dist/rules/sec/shared.js +8 -0
  191. package/dist/rules/str/index.d.ts +8 -0
  192. package/dist/rules/str/index.js +11 -0
  193. package/dist/rules/str/shared.d.ts +14 -0
  194. package/dist/rules/str/shared.js +16 -0
  195. package/dist/rules/str/str-1.d.ts +2 -0
  196. package/dist/rules/str/str-1.js +17 -0
  197. package/dist/rules/str/str-10.d.ts +2 -0
  198. package/dist/rules/str/str-10.js +15 -0
  199. package/dist/rules/str/str-2.d.ts +2 -0
  200. package/dist/rules/str/str-2.js +19 -0
  201. package/dist/rules/str/str-3.d.ts +2 -0
  202. package/dist/rules/str/str-3.js +14 -0
  203. package/dist/rules/str/str-4.d.ts +2 -0
  204. package/dist/rules/str/str-4.js +14 -0
  205. package/dist/rules/str/str-5.d.ts +2 -0
  206. package/dist/rules/str/str-5.js +19 -0
  207. package/dist/rules/str/str-6.d.ts +2 -0
  208. package/dist/rules/str/str-6.js +22 -0
  209. package/dist/rules/str/str-7.d.ts +2 -0
  210. package/dist/rules/str/str-7.js +18 -0
  211. package/dist/rules/str/str-8.d.ts +2 -0
  212. package/dist/rules/str/str-8.js +24 -0
  213. package/dist/rules/str/str-9.d.ts +2 -0
  214. package/dist/rules/str/str-9.js +14 -0
  215. package/dist/rules/util.d.ts +1 -1
  216. package/dist/trace.d.ts +20 -0
  217. package/dist/trace.js +77 -0
  218. package/dist/types.d.ts +86 -6
  219. package/package.json +4 -9
  220. package/dist/rules/links.d.ts +0 -2
  221. package/dist/rules/links.js +0 -95
  222. package/dist/rules/page.d.ts +0 -2
  223. package/dist/rules/page.js +0 -145
  224. package/dist/rules/site.d.ts +0 -2
  225. package/dist/rules/site.js +0 -177
package/dist/i18n/en.js CHANGED
@@ -1,151 +1,605 @@
1
1
  /**
2
2
  * English texts of the SEO checks: the reference for every other language. A new language is a file typed as
3
- * `SeoMessages` (TypeScript lists what is missing) plus a line in ./index.ts. AI search texts come from ai-search-auditor.
3
+ * `SeoMessages` (TypeScript lists what is missing) plus a line in ./index.ts.
4
4
  */
5
5
  export const en = {
6
6
  report: {
7
- title: (site) => `Search audit: ${site}`,
8
- clean: "Nothing to fix: search engines and AI search can read, understand and cite these pages.",
9
- footer: (pages, date) => `Checked ${pages} pages on ${date}. Also check by hand after each deploy: Google Search Console, Bing Webmaster Tools (ChatGPT search relies on Bing), Yandex Webmaster, and the live CDN with real crawlers.`,
10
- classic: "Classic search",
11
- ai: "AI search",
7
+ title: (site) => `SEO audit: ${site}`,
8
+ clean: "Nothing to fix: search engines can reach, index and show these pages.",
9
+ footer: (pages, date) => `Checked ${pages} pages on ${date}. Also check by hand after each deploy: Google Search Console, Bing Webmaster Tools, Yandex Webmaster, and the live CDN with real crawlers.`,
12
10
  },
13
11
  groups: {
14
12
  IDX: "Indexing",
15
- CAN: "Canonical",
16
- MAP: "Sitemap",
17
- META: "Titles and descriptions",
18
- STR: "Page structure",
19
- LNK: "Links",
20
- IMG: "Images",
21
- DATA: "Structured data",
22
- LANG: "Language versions",
23
- SEC: "HTTPS",
13
+ CAN: "Canonical: the main address of each page",
14
+ MAP: "Sitemap: the map of the site for search engines",
15
+ META: "Meta tags: how a page looks in search results",
16
+ STR: "Page structure: headings, language, mobile",
17
+ LNK: "Links: how pages link to each other",
18
+ IMG: "Images: image search and loading speed",
19
+ DATA: "Structured data: rich results",
20
+ LANG: "Language versions: hreflang for multilingual sites",
21
+ SEC: "Security and HTTPS",
24
22
  },
25
23
  rules: {
26
24
  "IDX-1": {
27
25
  title: "Pages answer 200",
26
+ todo: "Fix the failing pages or drop them from the sitemap",
28
27
  fix: "Fix the pages that answer an error or time out, or remove them from the sitemap and internal links. Search engines drop pages that keep failing.",
29
28
  impact: "Some pages answer an error, so search engines drop them from the results.",
30
29
  },
31
30
  "IDX-2": {
32
31
  title: "Missing pages answer 404",
32
+ todo: "Answer 404 for pages that do not exist",
33
33
  fix: "Answer 404 (or 410) for pages that do not exist, and do not show «not found» pages with 200. Otherwise search engines index empty pages and waste crawling on them.",
34
34
  impact: "Empty «not found» pages look like real ones, so search engines index junk and crawl the site less well.",
35
35
  },
36
+ "IDX-3": {
37
+ title: "robots.txt answers without errors",
38
+ todo: "Make robots.txt answer 200 with plain-text rules",
39
+ fix: "Serve robots.txt as plain text with 200, or 404 if you have none. A 5xx, 429 or timeout makes Google treat the whole site as closed until it answers again; HTML instead of rules, lines it cannot read and anything past 500 KB are ignored.",
40
+ impact: "Search engines may stop crawling the site or ignore its rules.",
41
+ },
42
+ "IDX-4": {
43
+ title: "robots.txt does not close pages from search engines",
44
+ todo: "Open the site's pages in robots.txt",
45
+ fix: "Remove the Disallow that covers these pages for Googlebot, Bingbot and Yandex, or take them out of the sitemap if they should stay closed. A page can be listed or blocked, not both; a whole site behind Disallow: / is not indexed at all.",
46
+ impact: "Search engines do not read closed pages, so they do not show them in results.",
47
+ },
48
+ "IDX-5": {
49
+ title: "The site has one version",
50
+ todo: "Redirect www and the other trailing slash to the main address",
51
+ fix: "Pick one host (with www. or without) and one form of address (with the trailing slash or without), and redirect the other with a 301. If both must answer, point the canonical of both at the main one.",
52
+ impact: "Search engines see two copies of the site and split its weight between them.",
53
+ },
54
+ "IDX-6": {
55
+ title: "Redirects take one step",
56
+ todo: "Redirect straight to the final address",
57
+ fix: "Make each redirect lead straight to the final address in one 301, and update the links and the sitemap to that address. Chains slow crawling; a loop makes the page unreachable.",
58
+ impact: "Every extra redirect slows crawling, and a loop makes the page unreachable.",
59
+ },
60
+ "IDX-7": {
61
+ title: "noindex does not close the home page or contradict the sitemap and canonical",
62
+ todo: "Decide for each page: indexed or closed",
63
+ fix: "A page in the sitemap must be indexable; a page with noindex must not be in the sitemap and must not be the target of a canonical. Do not combine noindex with a canonical to another page.",
64
+ impact: "Search engines get opposite signals and may drop the wrong page.",
65
+ },
66
+ "IDX-8": {
67
+ title: "robots.txt does not block CSS and JavaScript",
68
+ todo: "Open CSS and JavaScript to search engines in robots.txt",
69
+ fix: "Allow Googlebot to fetch the styles and scripts the page needs. Google renders the page before ranking it and sees a broken layout without them.",
70
+ impact: "Google sees a broken page and ranks it lower, especially on mobile.",
71
+ },
72
+ "IDX-9": {
73
+ title: "The server answers quickly",
74
+ todo: "Speed up the server response",
75
+ fix: "Aim for the first byte in under 0.8 s: cache pages on the CDN or serve static HTML, and check slow database queries. Google crawls slow sites less often.",
76
+ impact: "A slow server makes search engines crawl fewer pages, so new pages take longer to appear.",
77
+ },
36
78
  "CAN-1": {
37
79
  title: "Every page has a canonical",
80
+ todo: "Add a canonical to every page",
38
81
  fix: 'Add `<link rel="canonical" href="https://example.com/page">` with the page\'s own address to every page, so duplicates (parameters, trailing slashes) point at one version.',
39
82
  impact: "Without a canonical, copies of a page with parameters compete with each other in search.",
40
83
  },
41
84
  "CAN-2": {
42
85
  title: "The canonical is an absolute address on this site",
86
+ todo: "Make the canonical an absolute URL on your own domain",
43
87
  fix: "Use an absolute URL with https and the right host in the canonical. A relative or foreign canonical tells search engines to rank another page instead.",
44
88
  impact: "The canonical points to the wrong place, so search engines may rank another page or none.",
45
89
  },
46
90
  "CAN-3": {
47
91
  title: "The canonical leads to a page that works",
92
+ todo: "Point the canonical at a working final address",
48
93
  fix: "Point the canonical at the final address that answers 200, not at a redirect or an error.",
49
94
  impact: "The canonical leads to a broken or redirecting page, so the page itself may disappear from search.",
50
95
  },
96
+ "CAN-4": {
97
+ title: "One canonical per page",
98
+ todo: "Leave one canonical on the page",
99
+ fix: "Keep a single `<link rel=\"canonical\">`. When a page has two that disagree (often one from the CMS and one from an SEO plugin), Google ignores both.",
100
+ impact: "With two different canonicals, search engines ignore both and pick the main address themselves.",
101
+ },
102
+ "CAN-5": {
103
+ title: "The canonical sits in <head>",
104
+ todo: "Move the canonical into <head>",
105
+ fix: "Put `<link rel=\"canonical\">` in `<head>`. Google ignores a canonical in `<body>`, where scripts or a broken tag often push it.",
106
+ impact: "A canonical outside <head> is ignored, as if the page had none.",
107
+ },
108
+ "CAN-6": {
109
+ title: "The canonical in the HTML and in the HTTP header agree",
110
+ todo: "Make the canonical the same in the HTML and the Link header",
111
+ fix: "Name the same address in `<link rel=\"canonical\">` and in the `Link: <…>; rel=\"canonical\"` header, or drop one of them. Conflicting signals make Google pick on its own.",
112
+ impact: "Search engines get two different main addresses and may rank the wrong one.",
113
+ },
114
+ "CAN-7": {
115
+ title: "Canonicals lead straight to the main page",
116
+ todo: "Point the canonical straight at the final main page",
117
+ fix: "If page A names B as canonical, B must name itself, not C. Point every copy straight at the main page.",
118
+ impact: "Search engines may stop following the chain and rank a copy.",
119
+ },
120
+ "CAN-8": {
121
+ title: "Addresses with UTM tags name the clean address as canonical",
122
+ todo: "Build the canonical without the query parameters",
123
+ fix: "Generate the canonical from the page's own address, not from the requested one: with `?utm_source=…` the canonical must stay the clean address. Otherwise every campaign link creates a page of its own.",
124
+ impact: "Every link with UTM tags becomes a separate page in the index, and the weight spreads between copies.",
125
+ },
126
+ "CAN-9": {
127
+ title: "Language versions do not name each other as canonical",
128
+ todo: "Give each language version a canonical to itself",
129
+ fix: "Each translation names itself as canonical and links to the others with hreflang. A canonical to another language tells search engines this page is a copy, and it drops out of search in its own language.",
130
+ impact: "The translation drops out of search, and people searching in its language get the other version.",
131
+ },
51
132
  "MAP-1": {
52
133
  title: "There is a sitemap, and robots.txt points to it",
134
+ todo: "Add sitemap.xml and point robots.txt to it",
53
135
  fix: "Serve /sitemap.xml with every page you want in search and add `Sitemap: https://example.com/sitemap.xml` to robots.txt.",
54
136
  impact: "Search engines find new and deep pages later without a sitemap.",
55
137
  },
56
138
  "MAP-2": {
57
139
  title: "Sitemap addresses do not redirect",
140
+ todo: "Replace redirecting URLs in the sitemap with final ones",
58
141
  fix: "List the final addresses in the sitemap: the ones that answer 200 without redirects.",
59
142
  impact: "The sitemap sends search engines through redirects, which slows down indexing.",
60
143
  },
61
144
  "MAP-3": {
62
145
  title: "Sitemap pages are their own canonical",
146
+ todo: "Drop non-canonical pages from the sitemap",
63
147
  fix: "List only canonical pages in the sitemap. A page whose canonical points elsewhere is a duplicate and should not be there.",
64
148
  impact: "The sitemap lists duplicates, so search engines get mixed signals about which page to show.",
65
149
  },
150
+ "MAP-4": {
151
+ title: "The sitemap file is valid",
152
+ todo: "Fix the sitemap file",
153
+ fix: "Serve the sitemap as XML with 200: `<urlset>` with `<url><loc>…</loc></url>` entries, or a `<sitemapindex>` of such files. Escape & as `&amp;` inside addresses, keep each file under 50,000 addresses and 50 MB, and make every file in the index answer.",
154
+ impact: "Search engines cannot read the sitemap, or drop it, and find new pages only by chance.",
155
+ },
156
+ "MAP-5": {
157
+ title: "Sitemap addresses are full and on this site",
158
+ todo: "Write full addresses of this site in the sitemap",
159
+ fix: "List full addresses with the protocol and this site's host: `https://example.com/page`, not `/page` and not another domain. Search engines skip anything else.",
160
+ impact: "Search engines skip the addresses they cannot use, and those pages are found later or not at all.",
161
+ },
162
+ "MAP-6": {
163
+ title: "Pages the site links to are in the sitemap",
164
+ todo: "Add the linked pages to the sitemap",
165
+ fix: "List every page you want in search, not only the ones the generator knows: pages linked from the site but missing from the sitemap are found later. If a page should stay out of search, close it with noindex instead.",
166
+ impact: "Search engines find these pages later, only by following links.",
167
+ },
168
+ "MAP-7": {
169
+ title: "Every sitemap address answers",
170
+ todo: "Remove dead addresses from the sitemap",
171
+ fix: "Keep only pages that answer 200 in the sitemap: drop deleted pages or redirect them, and rebuild the sitemap from what is published.",
172
+ impact: "A sitemap full of dead pages teaches search engines to trust it less and wastes their crawling.",
173
+ },
174
+ "MAP-8": {
175
+ title: "lastmod in the sitemap is honest",
176
+ todo: "Put the real edit date of each page in lastmod",
177
+ fix: "Write lastmod in W3C format (`2026-11-09` or `2026-11-09T10:00:00+03:00`), from the date the page content really changed. Not in the future, and not the build time stamped on every address: Google learns to ignore such dates.",
178
+ impact: "Search engines ignore lastmod, so updated pages are recrawled later.",
179
+ },
66
180
  "META-1": {
67
181
  title: "Every page has a title of the right length",
182
+ todo: "Write a title of 10–60 characters",
68
183
  fix: "Give every page a unique <title> of about 10–60 characters with the main query first. Longer titles are cut in the results.",
69
184
  impact: "Missing or cut titles make the result less clickable and less relevant.",
70
185
  },
71
186
  "META-2": {
72
- title: "Every page has a description",
187
+ title: "Every page has a description of the right length",
188
+ todo: "Write a description of 50–160 characters",
73
189
  fix: 'Add `<meta name="description">` of about 50–160 characters: what the page offers and why to open it.',
74
- impact: "Without a description search engines pick a random snippet, and fewer people click.",
190
+ impact: "Without a description search engines pick a random snippet, a long one is cut off mid-sentence, and fewer people click.",
75
191
  },
76
192
  "META-3": {
77
193
  title: "Titles and descriptions are not repeated across pages",
194
+ todo: "Give every page its own title and description",
78
195
  fix: "Write a distinct title and description for each page. Repeated ones make pages look like duplicates.",
79
196
  impact: "Pages with the same title compete with each other, and search engines show the wrong one.",
80
197
  },
198
+ "META-4": {
199
+ title: "One title and one description per page",
200
+ todo: "Leave one title and one description in <head>",
201
+ fix: "Keep a single `<title>` and a single `<meta name=\"description\">` in `<head>`. Two of them usually come from the layout and the page both setting it; search engines then pick either.",
202
+ impact: "Search engines pick one of the titles or descriptions at random, not necessarily the one you wrote.",
203
+ },
204
+ "META-5": {
205
+ title: "The title names the page, not a template",
206
+ todo: "Write a title that says what the page is about",
207
+ fix: "Replace «Home», «Untitled», «React App» and similar with a title about this page: what it offers and to whom, with the main query first.",
208
+ impact: "A title like «Home» says nothing in the results, so the page gets few clicks and ranks for nothing.",
209
+ },
210
+ "META-6": {
211
+ title: "The description adds to the title",
212
+ todo: "Write a description that adds to the title",
213
+ fix: "Use the description to say what the title cannot: what is on the page, for whom, and why open it. Repeating the title wastes the snippet.",
214
+ impact: "The snippet repeats the title and gives no reason to click.",
215
+ },
216
+ "META-7": {
217
+ title: "The page declares its encoding",
218
+ todo: "Declare the encoding: <meta charset=\"utf-8\">",
219
+ fix: "Put `<meta charset=\"utf-8\">` first in `<head>`, or send `Content-Type: text/html; charset=utf-8`. Without it, Cyrillic and other non-Latin text may turn into gibberish in browsers and snippets.",
220
+ impact: "Text may show as gibberish in browsers and search results.",
221
+ },
222
+ "META-8": {
223
+ title: "The snippet is not forbidden",
224
+ todo: "Remove nosnippet and max-snippet:0",
225
+ fix: "Drop `nosnippet` and `max-snippet:0` from meta robots and X-Robots-Tag. They remove the description under the result, and Google keeps such pages out of AI Overviews.",
226
+ impact: "The result shows no description, and Google leaves the page out of AI answers.",
227
+ },
228
+ "META-9": {
229
+ title: "The site has a favicon",
230
+ todo: "Add a favicon",
231
+ fix: "Declare `<link rel=\"icon\" href=\"/favicon.svg\">` (or serve /favicon.ico), at least 48×48. Google shows it next to every result of the site.",
232
+ impact: "Results show a blank globe instead of your icon and stand out less.",
233
+ },
81
234
  "STR-1": {
82
235
  title: "One H1 per page",
236
+ todo: "Keep one H1 per page",
83
237
  fix: "Give each page exactly one H1 that says what the page is about; use H2–H6 for sections.",
84
238
  impact: "Without one clear H1, search engines read the topic of the page less reliably.",
85
239
  },
86
240
  "STR-2": {
87
241
  title: "Heading levels do not skip",
242
+ todo: "Order headings without skipping levels",
88
243
  fix: "Go down one level at a time: H1, then H2, then H3. Style headings with CSS, not by picking a level for its size.",
89
244
  impact: "A broken outline makes the structure of the page harder to read for search engines and screen readers.",
90
245
  },
91
246
  "STR-3": {
92
247
  title: "The page declares its language",
248
+ todo: "Set the language in <html lang>",
93
249
  fix: 'Set `<html lang="en">` (the page\'s real language) on every page.',
94
250
  impact: "Without a language, search engines and browsers may show the page to the wrong audience or offer to translate it.",
95
251
  },
96
252
  "STR-4": {
97
253
  title: "Pages are set up for mobile",
254
+ todo: "Add the viewport meta tag",
98
255
  fix: 'Add `<meta name="viewport" content="width=device-width, initial-scale=1">`. Google ranks by the mobile version of the page.',
99
256
  impact: "Without a mobile viewport the page renders as a shrunken desktop page, and mobile rankings suffer.",
100
257
  },
258
+ "STR-5": {
259
+ title: "Headings are not empty",
260
+ todo: "Put text in the H1 and the empty headings",
261
+ fix: "Give the H1 real text, not only a logo image: the image's alt is not heading text. Remove empty H2–H6 or fill them; they are often left by a template or used for spacing.",
262
+ impact: "An H1 without text reads as no H1, so search engines guess the topic of the page.",
263
+ },
264
+ "STR-6": {
265
+ title: "The title and the H1 are about the same thing",
266
+ todo: "Make the title and the H1 name the same topic",
267
+ fix: "Let the title and the H1 share the main words of the page. When they disagree, Google often rewrites the title in the results from the H1 or the text.",
268
+ impact: "Google may show its own title instead of yours.",
269
+ },
270
+ "STR-7": {
271
+ title: "The page can be zoomed on a phone",
272
+ todo: "Allow zoom in the viewport",
273
+ fix: "Remove `user-scalable=no` and keep `maximum-scale` at 2 or more (or drop it). People with weaker sight zoom to read; Lighthouse marks blocked zoom as an accessibility error.",
274
+ impact: "Some visitors cannot read the page on a phone, and the accessibility score drops.",
275
+ },
276
+ "STR-8": {
277
+ title: "<html lang> matches the language of the text",
278
+ todo: "Set <html lang> to the real language of the page",
279
+ fix: "Declare the language the page is written in: `<html lang=\"ru\">` for a Russian page. A wrong one sends the page to the wrong audience and makes browsers offer to translate it.",
280
+ impact: "Search engines may show the page to the wrong audience, and browsers offer to translate it.",
281
+ },
282
+ "STR-9": {
283
+ title: "The main content sits in <main>",
284
+ todo: "Wrap the main content in <main>",
285
+ fix: "Put the content of the page in `<main>`, apart from the header, navigation and footer. Search engines, AI search and screen readers then find the text that matters faster.",
286
+ impact: "Crawlers and screen readers tell the content from the menus less reliably.",
287
+ },
288
+ "STR-10": {
289
+ title: "The page starts with <!doctype html>",
290
+ todo: "Add <!doctype html> as the first line",
291
+ fix: "Start every page with `<!doctype html>`. Without it, browsers switch to quirks mode, and the layout may break in ways that differ between browsers.",
292
+ impact: "The page may render differently in different browsers, and look broken on some.",
293
+ },
101
294
  "LNK-1": {
102
295
  title: "Internal links work",
296
+ todo: "Fix the broken internal links",
103
297
  fix: "Fix or remove links that lead to errors. Broken links waste crawling and lose the link value they would pass.",
104
298
  impact: "Links lead to broken pages: visitors leave, and search engines crawl the site less.",
105
299
  },
106
300
  "LNK-2": {
107
301
  title: "Internal links do not go through redirects",
302
+ todo: "Link straight to the final address",
108
303
  fix: "Link straight to the final address (with the right trailing slash, host and https), not to one that redirects.",
109
304
  impact: "Every redirect in a link slows crawling and loses a little of the link's value.",
110
305
  },
306
+ "LNK-3": {
307
+ title: "Every page is linked from another page",
308
+ todo: "Link to the orphan pages from related pages",
309
+ fix: "Add links to these pages from the menu, a list or related pages. A page only the sitemap knows is crawled rarely and gets no weight from the rest of the site. If it is not needed, remove it from the sitemap.",
310
+ impact: "Pages nobody links to are crawled rarely and rank weakly.",
311
+ },
312
+ "LNK-4": {
313
+ title: "Links can be followed by a crawler",
314
+ todo: "Give every link a real href",
315
+ fix: "Use `<a href=\"/page\">` for navigation, not `<a>` without href, `href=\"#\"` or `javascript:` with a click handler. Crawlers follow href only; for actions that are not navigation, use `<button>`.",
316
+ impact: "Crawlers cannot follow these links, so the pages behind them are found later or not at all.",
317
+ },
318
+ "LNK-5": {
319
+ title: "Internal links do not carry nofollow",
320
+ todo: "Remove rel=\"nofollow\" from links to your own pages",
321
+ fix: "Keep `nofollow` for paid links and links you do not vouch for. On links to your own pages it stops weight from flowing inside the site and keeps the targets out of crawling.",
322
+ impact: "Your own pages get less weight and are crawled less.",
323
+ },
324
+ "LNK-6": {
325
+ title: "Link text says where the link goes",
326
+ todo: "Write link texts that name the target page",
327
+ fix: "Replace «read more», «here» and empty image links with text about the target: «Prices for teams», «How we test». Search engines use link text to learn what the target is about; screen readers read it out alone.",
328
+ impact: "Search engines learn less about the target pages, and screen-reader users hear «link, link».",
329
+ },
330
+ "LNK-7": {
331
+ title: "Links to other sites still work",
332
+ todo: "Fix or remove dead links to other sites",
333
+ fix: "Update links that lead to removed pages or sites that no longer exist, or link to an archive copy. Dead outbound links make a page look abandoned.",
334
+ impact: "Visitors hit dead ends, and the page looks abandoned to people and to search engines.",
335
+ },
111
336
  "IMG-1": {
112
337
  title: "Images have alt text",
338
+ todo: "Add alt text to images",
113
339
  fix: 'Add an `alt` that says what the image shows; use `alt=""` for decorative images. Image search and screen readers depend on it.',
114
340
  impact: "Images without alt are invisible to image search and to people using screen readers.",
115
341
  },
342
+ "IMG-2": {
343
+ title: "Images have width and height",
344
+ todo: "Set width and height on images",
345
+ fix: "Give every `<img>` its `width` and `height` (the real size; CSS can still scale it). The browser then keeps room for the image, and the layout does not jump as it loads (CLS, part of Core Web Vitals).",
346
+ impact: "The page jumps while images load: people misclick, and Core Web Vitals get worse.",
347
+ },
348
+ "IMG-3": {
349
+ title: "Lazy loading is used where it helps",
350
+ todo: "Load the first image at once and the rest lazily",
351
+ fix: "Do not put `loading=\"lazy\"` on the first image, usually the largest one on the first screen (LCP); add it to images further down a long page so they do not compete with what is visible.",
352
+ impact: "The main image appears later, or the first screen waits for images nobody sees yet.",
353
+ },
354
+ "IMG-4": {
355
+ title: "Photos are served in modern formats",
356
+ todo: "Serve photos as WebP or AVIF",
357
+ fix: "Serve WebP or AVIF, which are 25–50% lighter than JPEG and PNG at the same quality: through `<picture>` with a `<source type=\"image/avif\">`, an image CDN, or the framework's image component.",
358
+ impact: "Pages are heavier than they need to be and load slower on mobile.",
359
+ },
360
+ "IMG-5": {
361
+ title: "Images open",
362
+ todo: "Fix or remove broken images",
363
+ fix: "Fix the address of images that answer an error, or remove them. A broken image leaves a gap on the page and drops out of image search.",
364
+ impact: "Visitors see empty frames, and the images drop out of image search.",
365
+ },
366
+ "IMG-6": {
367
+ title: "Images are not heavier than they need to be",
368
+ todo: "Compress the heavy images",
369
+ fix: "Keep images under 500 KB: resize them to the size they are shown at, compress them, and offer smaller versions with `srcset` for phones.",
370
+ impact: "Heavy images slow the page down, especially on mobile, and LCP gets worse.",
371
+ },
372
+ "IMG-7": {
373
+ title: "Alt text describes the image",
374
+ todo: "Write alt text about what the image shows",
375
+ fix: "Describe what the image shows in a short sentence: «Team at the planning board», not «IMG_1234.jpg», «photo» or a whole paragraph. Use `alt=\"\"` for decoration.",
376
+ impact: "Image search and screen readers get a file name instead of a description.",
377
+ },
116
378
  "DATA-1": {
117
379
  title: "Structured data has no syntax errors",
380
+ todo: "Fix the JSON-LD syntax",
118
381
  fix: "Fix the JSON-LD blocks that do not parse: one error drops the whole block, and rich results with it.",
119
382
  impact: "Broken structured data is ignored, so the pages lose rich results in search.",
120
383
  },
384
+ "DATA-2": {
385
+ title: "Every block has @context and every entity a @type",
386
+ todo: "Add @context and @type to the JSON-LD",
387
+ fix: "Start every JSON-LD block with `\"@context\": \"https://schema.org\"` and give every entity a `@type` (Person, Product, Event…). Without them search engines do not know which vocabulary the data uses and skip it.",
388
+ impact: "Search engines skip the markup, and the page gets no rich result.",
389
+ },
390
+ "DATA-3": {
391
+ title: "Rich-result types have the properties Google requires",
392
+ todo: "Add the required properties to the markup",
393
+ fix: "Fill in what Google needs for the rich result of each type: a Product needs offers, a review or a rating; an Event needs startDate and location; a BreadcrumbList item needs a position. Check with the Rich Results Test.",
394
+ impact: "Without the required properties Google shows a plain result instead of a rich one.",
395
+ },
396
+ "DATA-4": {
397
+ title: "Nested pages have breadcrumbs",
398
+ todo: "Add BreadcrumbList to the nested pages",
399
+ fix: "Describe the path to the page in JSON-LD BreadcrumbList: Home › Blog › Article. Google then shows the trail instead of the bare address under the title.",
400
+ impact: "The result shows a bare address instead of a readable trail.",
401
+ },
402
+ "DATA-5": {
403
+ title: "Addresses and dates in the markup are readable",
404
+ todo: "Use full addresses and ISO dates in the markup",
405
+ fix: "Write addresses in full (`https://example.com/logo.png`, not `/logo.png`) and dates in ISO 8601 (`2026-11-09` or `2026-11-09T10:00:00+03:00`, not `09.11.2026`).",
406
+ impact: "Search engines cannot use the image, the link or the date, and the rich result loses them.",
407
+ },
408
+ "DATA-6": {
409
+ title: "The home page names the site with WebSite",
410
+ todo: "Add WebSite with the site name to the home page",
411
+ fix: "Put JSON-LD `{\"@type\": \"WebSite\", \"name\": \"…\", \"url\": \"https://example.com/\"}` on the home page. Google takes the site name shown above results from it.",
412
+ impact: "Google guesses the site name above your results, and may get it wrong.",
413
+ },
121
414
  "LANG-1": {
122
415
  title: "hreflang codes are valid",
416
+ todo: "Fix the hreflang codes",
123
417
  fix: "Use language codes like `en`, `ru`, `en-GB` or `x-default`. An invalid code makes search engines ignore the whole set.",
124
418
  impact: "Invalid language codes are ignored, so people get the wrong language version in search.",
125
419
  },
126
420
  "LANG-2": {
127
421
  title: "Language versions point at each other",
422
+ todo: "Make hreflang links point both ways",
128
423
  fix: "If page A lists page B as a translation, B must list A too. One-way hreflang links are ignored.",
129
424
  impact: "One-way language links are ignored, so search shows the wrong language version.",
130
425
  },
131
426
  "LANG-3": {
132
427
  title: "There is an x-default for other languages",
428
+ todo: "Add an hreflang x-default",
133
429
  fix: 'Add `<link rel="alternate" hreflang="x-default" href="…">` pointing at the version for everyone else.',
134
430
  impact: "Visitors whose language is not covered get a version chosen at random.",
135
431
  },
432
+ "LANG-4": {
433
+ title: "Every version lists itself in hreflang",
434
+ todo: "Add the page itself to its hreflang list",
435
+ fix: "List every language version on every one of them, the page itself included: the Russian page carries `hreflang=\"ru\"` to its own address next to the others.",
436
+ impact: "Search engines may ignore the language set and show the wrong version.",
437
+ },
438
+ "LANG-5": {
439
+ title: "hreflang leads to working canonical pages",
440
+ todo: "Point hreflang at the final canonical address of each version",
441
+ fix: "Link each language version by the address that answers 200 and names itself as canonical: not a redirect, not an error, not a copy whose canonical leads elsewhere.",
442
+ impact: "Search engines ignore language links to broken, redirecting or non-canonical pages.",
443
+ },
444
+ "LANG-6": {
445
+ title: "The hreflang language matches the page it points to",
446
+ todo: "Fix the language codes or the addresses in hreflang",
447
+ fix: "Make the code match the page: `hreflang=\"ru\"` must lead to the page with `<html lang=\"ru\">`. Usually two codes are swapped, or a template points every language at the same page.",
448
+ impact: "People searching in one language are sent to a page in another.",
449
+ },
450
+ "LANG-7": {
451
+ title: "Each language is listed once",
452
+ todo: "Leave one address per language in hreflang",
453
+ fix: "Give each language code one address. Two addresses for `en` usually mean a regional version needs its own code: `en-GB` and `en-US`.",
454
+ impact: "Search engines cannot tell which address to show for that language and may ignore both.",
455
+ },
456
+ "LANG-8": {
457
+ title: "hreflang addresses are full",
458
+ todo: "Write full addresses in hreflang",
459
+ fix: "Use addresses with the protocol and domain: `https://example.com/ru/`, not `/ru/`. Google does not accept relative ones in hreflang.",
460
+ impact: "Google ignores the relative language links.",
461
+ },
136
462
  "SEC-1": {
137
463
  title: "The site works over HTTPS, and http:// redirects to it",
464
+ todo: "Move the site to HTTPS and redirect http:// to it",
138
465
  fix: "Serve the site over https and redirect every http:// address to its https:// version with a 301.",
139
466
  impact: "Without HTTPS browsers mark the site as not secure, and search engines prefer secure competitors.",
140
467
  },
141
468
  "SEC-2": {
142
469
  title: "HTTPS pages do not load anything over http://",
470
+ todo: "Load every resource over https",
143
471
  fix: "Load images, scripts and styles over https. Browsers block or warn about mixed content.",
144
472
  impact: "Mixed content breaks parts of the page in browsers and makes the site look unsafe.",
145
473
  },
474
+ "SEC-3": {
475
+ title: "http:// redirects to https:// permanently",
476
+ todo: "Make the http:// redirect a 301",
477
+ fix: "Answer http:// with a 301 (or 308) to the https:// address. A 302 or 307 means «for now»: search engines keep the http:// version and do not pass its weight to https://.",
478
+ impact: "Search engines keep the old http:// address, and the https:// one does not get its weight.",
479
+ },
480
+ "SEC-4": {
481
+ title: "The certificate is valid and not about to expire",
482
+ todo: "Renew the TLS certificate and turn on auto-renewal",
483
+ fix: "Install a certificate that is trusted, matches the domain and is in date, and renew it automatically (Let's Encrypt with certbot, or the CDN's managed certificate). Browsers block a site with an expired or wrong certificate.",
484
+ impact: "Browsers show a full-page warning instead of the site, and visitors and crawlers leave.",
485
+ },
486
+ "SEC-5": {
487
+ title: "Browsers are told to use only https (HSTS)",
488
+ todo: "Add the Strict-Transport-Security header",
489
+ fix: "Send `Strict-Transport-Security: max-age=31536000; includeSubDomains` on https responses. Browsers then go straight to https://, skipping the redirect and the moment a connection can be intercepted.",
490
+ impact: "Every first visit goes through http:// first, which is slower and can be intercepted.",
491
+ },
492
+ "SEC-6": {
493
+ title: "The site's own addresses use https://",
494
+ todo: "Change http:// to https:// in links, canonical, sitemap and og tags",
495
+ fix: "Write every address of the site with https://: internal links, canonical, hreflang, og:url and og:image, and the sitemap. Each http:// address costs a redirect, and a canonical to it names the wrong version.",
496
+ impact: "Search engines and visitors go through needless redirects, and canonical points at the insecure version.",
497
+ },
498
+ "SEC-7": {
499
+ title: "Basic security headers are set",
500
+ todo: "Add nosniff, frame protection and Referrer-Policy headers",
501
+ fix: "Send `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN` (or CSP `frame-ancestors 'self'`) and `Referrer-Policy: strict-origin-when-cross-origin`. They do not change rankings, but protect visitors from common attacks.",
502
+ impact: "The site is easier to attack: content-type tricks, embedding in someone else's page, leaking addresses.",
503
+ },
146
504
  },
147
505
  /** Finding messages. URLs and quotes come in already formatted. */
148
506
  m: {
507
+ robotsStatus: (status) => `robots.txt answers ${status}: until it answers, Google treats the whole site as closed`,
508
+ robotsHtml: "robots.txt returns an HTML page instead of plain-text rules",
509
+ robotsSize: (kb) => `robots.txt is ${kb} KB: Google reads only the first 500 KB`,
510
+ robotsBadLines: (n, line) => `${n} ${n === 1 ? "line" : "lines"} search engines do not understand, e.g. «${line}»`,
511
+ blockedPage: (bots) => `robots.txt blocks it for ${bots}`,
512
+ blockedBySitemap: (bots) => `listed in the sitemap, but robots.txt blocks it for ${bots}`,
513
+ otherHostAnswers: (url, status) => `${url} answers ${status} instead of redirecting to the main address`,
514
+ slashDuplicate: (url) => `${url} also answers 200, with no redirect and no canonical to this page`,
515
+ redirectChain: (url, n, path) => `${url} takes ${n} redirects: ${path}`,
516
+ redirectLoop: (url, path) => `${url} redirects in a loop: ${path}`,
517
+ noindexHome: "the home page is closed with noindex: the site drops out of search",
518
+ noindexInSitemap: "listed in the sitemap, but closed with noindex",
519
+ noindexWithCanonical: (href) => `noindex together with a canonical to ${href}: two different signals`,
520
+ canonicalToNoindex: (href) => `the canonical leads to ${href}, which is closed with noindex`,
521
+ blockedResource: (url) => `robots.txt blocks ${url} for Googlebot`,
522
+ slowServer: (ms, limit) => `the home page starts to arrive after ${(ms / 1000).toFixed(1)} s (median of 3 requests), should be under ${limit / 1000} s`,
523
+ skipNoRobots: "no robots.txt",
524
+ skipNoSlashPage: "no page besides the home page among the audited ones",
525
+ temporaryRedirect: (status) => `answers ${status}, a temporary redirect, instead of 301`,
526
+ certExpired: (date) => `the certificate expired on ${date}`,
527
+ certInvalid: (error) => `browsers do not trust the certificate: ${error}`,
528
+ certExpiring: (days, date) => `the certificate expires in ${days} ${days === 1 ? "day" : "days"}, on ${date}`,
529
+ noHsts: "no Strict-Transport-Security header",
530
+ shortHsts: (days) => `HSTS max-age is ${days} ${days === 1 ? "day" : "days"}; at least 180 are needed`,
531
+ httpAddress: (where, url) => `${where} uses http://: ${url}`,
532
+ httpLinks: (n, url) => `${n} internal ${n === 1 ? "link" : "links"} over http://, e.g. ${url}`,
533
+ httpInSitemap: (n, url) => `${n} ${n === 1 ? "address" : "addresses"} over http://, e.g. ${url}`,
534
+ missingHeaders: (names) => `missing: ${names}`,
535
+ manyCanonicals: (n, hrefs) => `${n} different canonicals: ${hrefs}`,
536
+ canonicalInBody: (href) => `the canonical ${href} is in <body>`,
537
+ canonicalConflict: (html, header) => `the HTML names ${html}, the Link header names ${header}`,
538
+ canonicalChain: (target, next) => `the canonical leads to ${target}, whose own canonical is ${next}`,
539
+ utmNoCanonical: (url) => `${url} has no canonical`,
540
+ utmCanonical: (url) => `${url} names itself, with the UTM tags, as canonical`,
541
+ canonicalOtherLanguage: (href, to, from) => `the canonical leads to ${href}, the «${to}» version of this «${from}» page`,
542
+ sitemapStatus: (status) => `answers ${status}`,
543
+ sitemapHtml: "returns an HTML page instead of XML",
544
+ sitemapNotXml: "has neither <urlset> nor <sitemapindex>",
545
+ sitemapTooMany: (n) => `${n} addresses, over the limit of 50,000 per file`,
546
+ sitemapTooBig: (mb) => `${mb} MB, over the limit of 50 MB per file`,
547
+ sitemapAmpersand: (n) => `${n} ${n === 1 ? "address has" : "addresses have"} a bare & that breaks the XML`,
548
+ sitemapForeign: (n, example) => `${n} ${n === 1 ? "address is" : "addresses are"} relative or on another site, e.g. ${example}`,
549
+ notInSitemap: (n) => `linked from ${n} audited ${n === 1 ? "page" : "pages"}, but not in the sitemap`,
550
+ sitemapDead: (status) => `listed in the sitemap, but answers ${status}`,
551
+ lastmodBad: (n, example) => `${n} lastmod ${n === 1 ? "value is" : "values are"} not a W3C date, e.g. «${example}»`,
552
+ lastmodFuture: (n, example) => `${n} lastmod ${n === 1 ? "date is" : "dates are"} in the future, e.g. ${example}`,
553
+ lastmodSame: (n, value) => `all ${n} addresses have the same lastmod, ${value}: it looks like the build time`,
554
+ skipNoLastmod: "the sitemap has no lastmod",
555
+ manyTitles: (n) => `${n} <title> tags in <head>`,
556
+ manyDescriptions: (n) => `${n} meta descriptions in <head>`,
557
+ genericTitle: (title) => `the title is «${title}»`,
558
+ descriptionRepeatsTitle: "the description repeats the title",
559
+ noCharset: "no charset in the Content-Type header or in <meta charset>",
560
+ noSnippet: (rule) => `the snippet is forbidden: ${rule}`,
561
+ noFavicon: (status) => `no icon in <head>, and /favicon.ico answers ${status}`,
562
+ emptyH1: "the H1 has no text: only an image or nothing",
563
+ emptyHeadings: (n, levels) => `${n} empty ${n === 1 ? "heading" : "headings"}: ${levels}`,
564
+ titleH1Apart: (title, h1) => `no word in common: title «${title}», H1 «${h1}»`,
565
+ noZoom: (viewport) => `zoom is blocked: ${viewport}`,
566
+ langMismatch: (lang, script, percent) => `<html lang="${lang}">, but ${percent}% of the letters are ${script}`,
567
+ scriptCyrillic: "Cyrillic",
568
+ scriptLatin: "Latin",
569
+ noMain: "no <main> on the page",
570
+ noDoctype: "no <!doctype html> at the start",
571
+ orphan: "no audited page links here: only the sitemap knows this page",
572
+ uncrawlableLinks: (n, text) => `${n} ${n === 1 ? "link has" : "links have"} no address to follow (no href, # or javascript:), e.g. «${text}»`,
573
+ nofollowInternal: (n, url) => `${n} internal ${n === 1 ? "link" : "links"} with nofollow, e.g. to ${url}`,
574
+ vagueLinks: (n, text) => `${n} ${n === 1 ? "link says" : "links say"} nothing about the target, e.g. «${text}»`,
575
+ namelessLinks: (n, href) => `${n} ${n === 1 ? "link has" : "links have"} no text, label or alt, e.g. to ${href}`,
576
+ deadExternal: (url, status) => `link to ${url} answers ${status}`,
577
+ skipPartialSample: "not every sitemap page was audited: run with --pages all",
578
+ skipNoExternal: "no links to other sites on the audited pages",
579
+ skipNoNetwork: "no outside site answered: the audit has no internet access",
580
+ imagesNoSize: (n, total, src) => `${n} of ${total} images without width and height, e.g. ${src}`,
581
+ firstImageLazy: (src) => `the first image loads lazily: ${src}`,
582
+ noLazyImages: (n) => `${n} images, none of them lazy`,
583
+ oldFormats: (n, src) => `${n} ${n === 1 ? "image is" : "images are"} JPEG or PNG only, e.g. ${src}`,
584
+ brokenImage: (src, status) => `image ${src} answers ${status}`,
585
+ heavyImage: (src, kb, max) => `image ${src} is ${kb} KB, should be under ${max} KB`,
586
+ poorAlt: (n, alt) => `${n} alt ${n === 1 ? "text does" : "texts do"} not describe the image, e.g. «${alt}»`,
587
+ noContext: "a JSON-LD block without a schema.org @context",
588
+ noType: (n) => `${n} ${n === 1 ? "entity has" : "entities have"} no @type`,
589
+ missingProperties: (type, keys) => `${type} without ${keys}`,
590
+ noBreadcrumbs: "no BreadcrumbList",
591
+ relativeInMarkup: (n, example) => `${n} ${n === 1 ? "address is" : "addresses are"} not full, e.g. ${example}`,
592
+ badDatesInMarkup: (n, example) => `${n} ${n === 1 ? "date is" : "dates are"} not ISO 8601, e.g. ${example}`,
593
+ noWebSite: "no WebSite with a name on the home page",
594
+ skipNoRichTypes: "no types with rich results on the audited pages",
595
+ skipNoNestedPages: "no nested pages among the audited ones",
596
+ noSelfHreflang: "the page is not in its own hreflang list",
597
+ hreflangStatus: (lang, url, status) => `«${lang}» leads to ${url}, which answers ${status}`,
598
+ hreflangRedirects: (lang, url, to) => `«${lang}» leads to ${url}, which redirects to ${to}`,
599
+ hreflangNotCanonical: (lang, url, canonical) => `«${lang}» leads to ${url}, whose canonical is ${canonical}`,
600
+ hreflangLangMismatch: (lang, url, htmlLang) => `«${lang}» leads to ${url}, a page with lang="${htmlLang}"`,
601
+ duplicateHreflang: (codes) => `more than one address for ${codes}`,
602
+ relativeHreflang: (n, href) => `${n} relative ${n === 1 ? "address" : "addresses"}, e.g. ${href}`,
149
603
  status: (status) => `answers ${status}`,
150
604
  soft404Probe: (url) => `a made-up address ${url} answers 200 instead of 404`,
151
605
  soft404Page: (title) => `answers 200 but looks like a «not found» page: «${title}»`,
@@ -159,18 +613,18 @@ export const en = {
159
613
  sitemapRedirects: (to) => `listed in the sitemap, redirects to ${to}`,
160
614
  sitemapCanonicalElsewhere: (href) => `listed in the sitemap, but its canonical is ${href}`,
161
615
  noTitle: "no <title>",
162
- titleLength: (n, title) => `title of ${n} characters: «${title}»`,
616
+ titleLength: (n, title, min, max) => `title of ${n} characters, should be ${min}–${max}: «${title}»`,
163
617
  noDescription: "no meta description",
164
- descriptionLength: (n) => `description of ${n} characters`,
165
- sameTitle: (n, title) => `the same title on ${n} pages: «${title}»`,
166
- sameDescription: (n, text) => `the same description on ${n} pages: «${text}»`,
618
+ descriptionLength: (n, min, max) => `description of ${n} characters, should be ${min}–${max}`,
619
+ sameTitle: (n, title) => `the same title on ${n} pages, each page needs its own: «${title}»`,
620
+ sameDescription: (n, text) => `the same description on ${n} pages, each page needs its own: «${text}»`,
167
621
  noH1: "no H1",
168
- manyH1: (n) => `${n} H1 headings`,
169
- skippedLevel: (from, to, text) => `H${from} is followed by H${to}: «${text}»`,
622
+ manyH1: (n) => `${n} H1 headings, a page needs one`,
623
+ skippedLevel: (from, to, text) => `H${from} is followed by H${to}, skipping a level: «${text}»`,
170
624
  noLang: "no <html lang>",
171
625
  noViewport: "no viewport meta tag",
172
- brokenLink: (status, from) => `answers ${status}, linked from ${from}`,
173
- redirectLink: (to, from) => `redirects to ${to}, linked from ${from}`,
626
+ brokenLink: (url, status) => `link to ${url} answers ${status}`,
627
+ redirectLink: (url, to) => `link to ${url} redirects to ${to}`,
174
628
  linksNotChecked: (checked, found) => `${checked} of ${found} internal links checked; raise --max-links to check more`,
175
629
  imagesNoAlt: (n, total, example) => `${n} of ${total} images without alt, e.g. ${example}`,
176
630
  jsonLdError: (error) => `JSON-LD does not parse: ${error}`,
@@ -185,6 +639,7 @@ export const en = {
185
639
  skipNoHreflang: "the site has no hreflang",
186
640
  skipOneLanguage: "the site has one language",
187
641
  skipNoImages: "no images on the audited pages",
642
+ skipNoOwnImages: "no images of this site on the audited pages: those of other sites are not requested",
188
643
  skipNoJsonLd: "no structured data on the audited pages",
189
644
  skipNoLinks: "no internal links on the audited pages",
190
645
  skipNotHttps: "the site is not on https",