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/ru.js CHANGED
@@ -1,146 +1,605 @@
1
+ /** «символ», «символа», «символов» for a count. */
2
+ const chars = (n) => {
3
+ const d = n % 10, h = n % 100;
4
+ return d === 1 && h !== 11 ? "символ" : d >= 2 && d <= 4 && (h < 12 || h > 14) ? "символа" : "символов";
5
+ };
1
6
  export const ru = {
2
7
  report: {
3
- title: (site) => `Аудит для поиска: ${site}`,
4
- clean: "Исправлять нечего: поисковики и AI-поиск могут прочитать эти страницы, понять их и сослаться на них.",
5
- footer: (pages, date) => `Проверено страниц: ${pages}, ${date}. После каждого деплоя проверьте и вручную: Google Search Console, Bing Webmaster Tools (на Bing опирается поиск ChatGPT), Яндекс Вебмастер и живой CDN реальными краулерами.`,
6
- classic: "Классический поиск",
7
- ai: "AI-поиск",
8
+ title: (site) => `SEO-аудит: ${site}`,
9
+ clean: "Исправлять нечего: поисковики могут найти эти страницы, проиндексировать их и показать в выдаче.",
10
+ footer: (pages, date) => `Проверено страниц: ${pages}, ${date}. После каждого деплоя проверьте и вручную: Google Search Console, Bing Webmaster Tools, Яндекс Вебмастер и живой CDN реальными краулерами.`,
8
11
  },
9
12
  groups: {
10
13
  IDX: "Индексация",
11
- CAN: "Canonical",
12
- MAP: "Sitemap",
13
- META: "Мета-теги",
14
- STR: "Структура страницы",
15
- LNK: "Ссылки",
16
- IMG: "Картинки",
17
- DATA: "Структурированные данные",
18
- LANG: "Языковые версии",
19
- SEC: "HTTPS",
14
+ CAN: "Canonical: основной адрес страницы",
15
+ MAP: "Sitemap: карта сайта для поисковиков",
16
+ META: "Мета-теги: как страница выглядит в выдаче",
17
+ STR: "Структура страницы: заголовки, язык, мобильная версия",
18
+ LNK: "Ссылки: как страницы связаны между собой",
19
+ IMG: "Картинки: поиск по картинкам и скорость загрузки",
20
+ DATA: "Структурированные данные: расширенные сниппеты",
21
+ LANG: "Языковые версии: hreflang для многоязычных сайтов",
22
+ SEC: "Безопасность и HTTPS",
20
23
  },
21
24
  rules: {
22
25
  "IDX-1": {
23
26
  title: "Страницы отвечают 200",
27
+ todo: "Почините страницы с ошибками или уберите их из sitemap",
24
28
  fix: "Почините страницы, которые отвечают ошибкой или не отвечают, либо уберите их из sitemap и внутренних ссылок. Поисковики выкидывают страницы, которые стабильно не открываются.",
25
29
  impact: "Часть страниц отвечает ошибкой, и поисковики убирают их из выдачи.",
26
30
  },
27
31
  "IDX-2": {
28
32
  title: "Несуществующие страницы отвечают 404",
33
+ todo: "Отдавайте 404 на несуществующие адреса",
29
34
  fix: "Отдавайте 404 (или 410) на несуществующие адреса и не показывайте страницу «не найдено» с кодом 200. Иначе поисковики индексируют пустые страницы и тратят на них обход.",
30
35
  impact: "Пустые страницы «не найдено» выглядят как настоящие: в индекс попадает мусор, а сайт обходят хуже.",
31
36
  },
37
+ "IDX-3": {
38
+ title: "robots.txt отвечает без ошибок",
39
+ todo: "Сделайте, чтобы robots.txt отвечал 200 и текстовыми правилами",
40
+ fix: "Отдавайте robots.txt текстом с кодом 200 или 404, если его нет. При 5xx, 429 или таймауте Google считает весь сайт закрытым, пока файл не ответит; HTML вместо правил, непонятные строки и всё после 500 КБ поисковики пропускают.",
41
+ impact: "Поисковики могут перестать обходить сайт или проигнорировать его правила.",
42
+ },
43
+ "IDX-4": {
44
+ title: "robots.txt не закрывает страницы от поисковиков",
45
+ todo: "Откройте страницы сайта в robots.txt",
46
+ fix: "Уберите Disallow, который закрывает эти страницы для Googlebot, Bingbot и Яндекса, или уберите страницы из sitemap, если они должны оставаться закрытыми. Страница может быть либо в sitemap, либо закрыта, но не то и другое сразу; сайт за Disallow: / не индексируется вовсе.",
47
+ impact: "Закрытые страницы поисковик не читает и не показывает в выдаче.",
48
+ },
49
+ "IDX-5": {
50
+ title: "У сайта одна версия",
51
+ todo: "Настройте редирект с www и второго варианта слеша на основной адрес",
52
+ fix: "Выберите один домен (с www. или без) и одну форму адреса (со слешем в конце или без) и перенаправляйте вторую версию с кодом 301. Если обе должны отвечать, укажите в canonical обеих основной адрес.",
53
+ impact: "Поисковики видят две копии сайта и делят его вес между ними.",
54
+ },
55
+ "IDX-6": {
56
+ title: "Редиректы ведут в один шаг",
57
+ todo: "Ведите редиректы сразу на конечный адрес",
58
+ fix: "Пусть каждый редирект ведёт на конечный адрес одним 301, а ссылки и sitemap указывают сразу на него. Цепочки замедляют обход, а петля делает страницу недоступной.",
59
+ impact: "Каждый лишний редирект замедляет обход, а петля делает страницу недоступной.",
60
+ },
61
+ "IDX-7": {
62
+ title: "noindex не закрывает главную и не противоречит sitemap и canonical",
63
+ todo: "Решите для каждой страницы: в индексе она или закрыта",
64
+ fix: "Страница из sitemap должна быть открыта для индексации; страница с noindex не должна быть в sitemap и не должна быть целью canonical. Не сочетайте noindex с canonical на другую страницу.",
65
+ impact: "Поисковики получают противоположные сигналы и могут выкинуть не ту страницу.",
66
+ },
67
+ "IDX-8": {
68
+ title: "robots.txt не закрывает CSS и JavaScript",
69
+ todo: "Откройте поисковикам CSS и JavaScript в robots.txt",
70
+ fix: "Разрешите Googlebot загружать стили и скрипты, без которых страница не отрисуется. Google рендерит страницу перед ранжированием и без них видит сломанную вёрстку.",
71
+ impact: "Google видит сломанную страницу и ранжирует её ниже, особенно в мобильной выдаче.",
72
+ },
73
+ "IDX-9": {
74
+ title: "Сервер отвечает быстро",
75
+ todo: "Ускорьте ответ сервера",
76
+ fix: "Добивайтесь первого байта быстрее 0,8 с: кэшируйте страницы на CDN или отдавайте статический HTML, проверьте медленные запросы к базе. Медленные сайты Google обходит реже.",
77
+ impact: "Медленный сервер заставляет поисковики обходить меньше страниц, и новые появляются в выдаче позже.",
78
+ },
32
79
  "CAN-1": {
33
80
  title: "У каждой страницы есть canonical",
81
+ todo: "Добавьте canonical на каждую страницу",
34
82
  fix: 'Добавьте на каждую страницу `<link rel="canonical" href="https://example.com/page">` с её собственным адресом, чтобы дубли (параметры, слеш в конце) указывали на одну версию.',
35
83
  impact: "Без canonical копии страницы с параметрами конкурируют друг с другом в поиске.",
36
84
  },
37
85
  "CAN-2": {
38
86
  title: "Canonical — абсолютный адрес на этом сайте",
87
+ todo: "Сделайте canonical полным адресом на своём домене",
39
88
  fix: "Указывайте в canonical полный адрес с https и правильным доменом. Относительный или чужой canonical велит поисковику ранжировать другую страницу.",
40
89
  impact: "Canonical указывает не туда, и поисковик может показывать другую страницу или никакую.",
41
90
  },
42
91
  "CAN-3": {
43
92
  title: "Canonical ведёт на рабочую страницу",
93
+ todo: "Направьте canonical на рабочий конечный адрес",
44
94
  fix: "Ставьте в canonical конечный адрес, который отвечает 200, а не редирект и не ошибку.",
45
95
  impact: "Canonical ведёт на сломанную страницу или редирект, и сама страница может пропасть из поиска.",
46
96
  },
97
+ "CAN-4": {
98
+ title: "Один canonical на странице",
99
+ todo: "Оставьте на странице один canonical",
100
+ fix: "Оставьте один `<link rel=\"canonical\">`. Когда их два и они разные (часто один от CMS, второй от SEO-плагина), Google не учитывает оба.",
101
+ impact: "При двух разных canonical поисковики не учитывают ни один и выбирают основной адрес сами.",
102
+ },
103
+ "CAN-5": {
104
+ title: "Canonical стоит в <head>",
105
+ todo: "Перенесите canonical в <head>",
106
+ fix: "Ставьте `<link rel=\"canonical\">` в `<head>`. Canonical в `<body>`, куда его часто выталкивают скрипты или незакрытый тег, Google не учитывает.",
107
+ impact: "Canonical вне <head> не учитывается, как будто его нет.",
108
+ },
109
+ "CAN-6": {
110
+ title: "Canonical в HTML и в HTTP-заголовке совпадают",
111
+ todo: "Сделайте canonical одинаковым в HTML и в заголовке Link",
112
+ fix: "Указывайте один и тот же адрес в `<link rel=\"canonical\">` и в заголовке `Link: <…>; rel=\"canonical\"` или оставьте что-то одно. При противоречии Google выбирает сам.",
113
+ impact: "Поисковики получают два разных основных адреса и могут показывать не тот.",
114
+ },
115
+ "CAN-7": {
116
+ title: "Canonical ведёт сразу на основную страницу",
117
+ todo: "Ведите canonical сразу на конечную основную страницу",
118
+ fix: "Если страница A указывает canonical на B, то B должна указывать на себя, а не на C. Указывайте в canonical каждой копии сразу основную страницу.",
119
+ impact: "Поисковики могут не пройти цепочку до конца и показывать копию.",
120
+ },
121
+ "CAN-8": {
122
+ title: "Адрес с UTM-метками указывает canonical на чистый адрес",
123
+ todo: "Формируйте canonical без параметров адреса",
124
+ fix: "Собирайте canonical из адреса самой страницы, а не из запрошенного: с `?utm_source=…` canonical должен оставаться чистым адресом. Иначе каждая ссылка из рекламы создаёт отдельную страницу.",
125
+ impact: "Каждая ссылка с UTM-метками становится отдельной страницей в индексе, и вес расходится по копиям.",
126
+ },
127
+ "CAN-9": {
128
+ title: "Языковые версии не указывают canonical друг на друга",
129
+ todo: "Укажите в canonical каждой языковой версии её саму",
130
+ fix: "Каждый перевод указывает canonical на себя и ссылается на остальные через hreflang. Canonical на другой язык говорит поисковику, что страница — копия, и она пропадает из выдачи на своём языке.",
131
+ impact: "Перевод пропадает из выдачи, и люди, которые ищут на его языке, получают другую версию.",
132
+ },
47
133
  "MAP-1": {
48
134
  title: "Есть sitemap, и robots.txt на него указывает",
135
+ todo: "Добавьте sitemap.xml и укажите его в robots.txt",
49
136
  fix: "Отдавайте /sitemap.xml со всеми страницами, которые должны быть в поиске, и добавьте в robots.txt строку `Sitemap: https://example.com/sitemap.xml`.",
50
137
  impact: "Без sitemap поисковики позже находят новые и глубокие страницы.",
51
138
  },
52
139
  "MAP-2": {
53
140
  title: "Адреса в sitemap не редиректят",
141
+ todo: "Замените в sitemap адреса с редиректом на конечные",
54
142
  fix: "Указывайте в sitemap конечные адреса, которые отвечают 200 без редиректов.",
55
143
  impact: "Sitemap гоняет поисковики через редиректы, и индексация замедляется.",
56
144
  },
57
145
  "MAP-3": {
58
146
  title: "Страницы из sitemap канонические",
147
+ todo: "Уберите из sitemap неканонические страницы",
59
148
  fix: "Держите в sitemap только канонические страницы. Страница, чей canonical указывает на другую, — дубль, ей там не место.",
60
149
  impact: "В sitemap есть дубли, и поисковик получает противоречивые сигналы, какую страницу показывать.",
61
150
  },
151
+ "MAP-4": {
152
+ title: "Файл sitemap корректный",
153
+ todo: "Исправьте файл sitemap",
154
+ fix: "Отдавайте sitemap как XML с кодом 200: `<urlset>` с записями `<url><loc>…</loc></url>` или `<sitemapindex>` со списком таких файлов. Экранируйте & как `&amp;` внутри адресов, держите в файле не больше 50 000 адресов и 50 МБ, и пусть каждый файл из индекса отвечает.",
155
+ impact: "Поисковики не могут прочитать sitemap или выбрасывают его и находят новые страницы только случайно.",
156
+ },
157
+ "MAP-5": {
158
+ title: "Адреса в sitemap полные и с этого сайта",
159
+ todo: "Пишите в sitemap полные адреса этого сайта",
160
+ fix: "Указывайте полные адреса с протоколом и доменом этого сайта: `https://example.com/page`, а не `/page` и не другой домен. Остальное поисковики пропускают.",
161
+ impact: "Поисковики пропускают адреса, которые не могут использовать, и эти страницы находятся позже или никогда.",
162
+ },
163
+ "MAP-6": {
164
+ title: "Страницы, на которые ссылается сайт, есть в sitemap",
165
+ todo: "Добавьте страницы со ссылками на них в sitemap",
166
+ fix: "Указывайте в sitemap все страницы, которые должны быть в поиске, а не только те, что знает генератор: страницы со ссылками на сайте, но без записи в sitemap, находятся позже. Если страница не должна попасть в поиск, закройте её noindex.",
167
+ impact: "Поисковики находят эти страницы позже и только по ссылкам.",
168
+ },
169
+ "MAP-7": {
170
+ title: "Все адреса sitemap отвечают",
171
+ todo: "Уберите из sitemap мёртвые адреса",
172
+ fix: "Оставляйте в sitemap только страницы, которые отвечают 200: удалённые уберите или перенаправьте, а sitemap собирайте из опубликованного.",
173
+ impact: "Sitemap с мёртвыми страницами приучает поисковики меньше ему доверять и тратит их обход впустую.",
174
+ },
175
+ "MAP-8": {
176
+ title: "lastmod в sitemap честный",
177
+ todo: "Ставьте в lastmod настоящую дату правки страницы",
178
+ fix: "Пишите lastmod в формате W3C (`2026-11-09` или `2026-11-09T10:00:00+03:00`) по дате, когда содержимое страницы действительно менялось. Не в будущем и не время сборки на всех адресах сразу: такие даты Google перестаёт учитывать.",
179
+ impact: "Поисковики не учитывают lastmod, и обновлённые страницы переобходятся позже.",
180
+ },
62
181
  "META-1": {
63
182
  title: "У каждой страницы есть title нужной длины",
183
+ todo: "Напишите title на 10–60 символов",
64
184
  fix: "Дайте каждой странице уникальный <title> примерно на 10–60 символов, главный запрос — в начале. Длинные заголовки обрезаются в выдаче.",
65
185
  impact: "Без заголовка или с обрезанным заголовком страница хуже кликается и хуже соответствует запросу.",
66
186
  },
67
187
  "META-2": {
68
- title: "У каждой страницы есть description",
188
+ title: "У каждой страницы есть description нужной длины",
189
+ todo: "Напишите description на 50–160 символов",
69
190
  fix: 'Добавьте `<meta name="description">` примерно на 50–160 символов: что на странице и зачем её открывать.',
70
- impact: "Без описания поисковик берёт случайный кусок текста, и кликают реже.",
191
+ impact: "Без описания поисковик берёт случайный кусок текста, длинное обрезает на полуслове, и кликают реже.",
71
192
  },
72
193
  "META-3": {
73
194
  title: "Title и description не повторяются на разных страницах",
195
+ todo: "Сделайте title и description своими на каждой странице",
74
196
  fix: "Напишите для каждой страницы свой заголовок и описание. Одинаковые делают страницы похожими на дубли.",
75
197
  impact: "Страницы с одинаковым заголовком конкурируют между собой, и в выдаче оказывается не та.",
76
198
  },
199
+ "META-4": {
200
+ title: "Один title и одно description на странице",
201
+ todo: "Оставьте в <head> один title и одно description",
202
+ fix: "Оставьте в `<head>` один `<title>` и одно `<meta name=\"description\">`. Два обычно появляются, когда их задают и общий шаблон, и сама страница; поисковик тогда берёт любое.",
203
+ impact: "Поисковик берёт любой из заголовков или описаний, не обязательно тот, что вы написали.",
204
+ },
205
+ "META-5": {
206
+ title: "Title говорит о странице, а не о шаблоне",
207
+ todo: "Напишите title о том, чему посвящена страница",
208
+ fix: "Замените «Главная», «Без названия», «React App» и подобное на заголовок об этой странице: что на ней и для кого, главный запрос — в начале.",
209
+ impact: "Заголовок вроде «Главная» ничего не говорит в выдаче: страницу мало кликают, и она ни по чему не ранжируется.",
210
+ },
211
+ "META-6": {
212
+ title: "Description дополняет title",
213
+ todo: "Напишите description, который дополняет title",
214
+ fix: "Пишите в description то, чего нет в заголовке: что на странице, для кого и зачем её открывать. Повтор заголовка тратит сниппет впустую.",
215
+ impact: "Сниппет повторяет заголовок и не даёт причины кликнуть.",
216
+ },
217
+ "META-7": {
218
+ title: "Страница объявляет кодировку",
219
+ todo: "Объявите кодировку: <meta charset=\"utf-8\">",
220
+ fix: "Поставьте `<meta charset=\"utf-8\">` первым в `<head>` или отдавайте `Content-Type: text/html; charset=utf-8`. Без этого кириллица может превратиться в кракозябры в браузере и в сниппете.",
221
+ impact: "Текст может отображаться кракозябрами в браузере и в выдаче.",
222
+ },
223
+ "META-8": {
224
+ title: "Сниппет не запрещён",
225
+ todo: "Уберите nosnippet и max-snippet:0",
226
+ fix: "Уберите `nosnippet` и `max-snippet:0` из meta robots и X-Robots-Tag. Они убирают описание под результатом, а такие страницы Google не берёт в AI-обзоры.",
227
+ impact: "У результата нет описания, и Google не берёт страницу в AI-ответы.",
228
+ },
229
+ "META-9": {
230
+ title: "У сайта есть favicon",
231
+ todo: "Добавьте favicon",
232
+ fix: "Укажите `<link rel=\"icon\" href=\"/favicon.svg\">` (или отдавайте /favicon.ico) размером от 48×48. Google показывает его рядом с каждым результатом сайта.",
233
+ impact: "В выдаче вместо вашей иконки пустой глобус, и результат заметен хуже.",
234
+ },
77
235
  "STR-1": {
78
236
  title: "Один H1 на странице",
237
+ todo: "Оставьте на странице один H1",
79
238
  fix: "Дайте каждой странице ровно один H1 о том, чему она посвящена; разделы — H2–H6.",
80
239
  impact: "Без одного ясного H1 поисковик хуже понимает тему страницы.",
81
240
  },
82
241
  "STR-2": {
83
242
  title: "Уровни заголовков идут по порядку",
243
+ todo: "Выстройте заголовки по уровням без пропусков",
84
244
  fix: "Спускайтесь на один уровень: H1, затем H2, затем H3. Размер задавайте в CSS, а не выбором уровня.",
85
245
  impact: "Сломанная структура заголовков мешает поисковикам и экранным дикторам понять страницу.",
86
246
  },
87
247
  "STR-3": {
88
248
  title: "Страница объявляет свой язык",
249
+ todo: "Укажите язык в <html lang>",
89
250
  fix: 'Укажите на каждой странице `<html lang="ru">` с её настоящим языком.',
90
251
  impact: "Без языка поисковики и браузеры могут показать страницу не той аудитории или предложить перевести.",
91
252
  },
92
253
  "STR-4": {
93
254
  title: "Страницы настроены для телефонов",
255
+ todo: "Добавьте meta viewport",
94
256
  fix: 'Добавьте `<meta name="viewport" content="width=device-width, initial-scale=1">`. Google ранжирует по мобильной версии страницы.',
95
257
  impact: "Без viewport страница на телефоне выглядит как уменьшенная десктопная, и мобильная выдача страдает.",
96
258
  },
259
+ "STR-5": {
260
+ title: "Заголовки не пустые",
261
+ todo: "Добавьте текст в H1 и в пустые заголовки",
262
+ fix: "Дайте H1 настоящий текст, а не только картинку-логотип: alt картинки не считается текстом заголовка. Пустые H2–H6 уберите или заполните; их часто оставляет шаблон или используют для отступов.",
263
+ impact: "H1 без текста читается как отсутствие H1, и поисковик угадывает тему страницы.",
264
+ },
265
+ "STR-6": {
266
+ title: "Title и H1 об одном и том же",
267
+ todo: "Сделайте, чтобы title и H1 называли одну тему",
268
+ fix: "Пусть в title и H1 будут общие главные слова страницы. Когда они расходятся, Google часто переписывает заголовок в выдаче по H1 или по тексту.",
269
+ impact: "Google может показать в выдаче свой заголовок вместо вашего.",
270
+ },
271
+ "STR-7": {
272
+ title: "Страницу можно увеличить на телефоне",
273
+ todo: "Разрешите масштабирование во viewport",
274
+ fix: "Уберите `user-scalable=no` и оставьте `maximum-scale` не меньше 2 (или уберите его). Люди со слабым зрением увеличивают текст, а Lighthouse считает запрет масштаба ошибкой доступности.",
275
+ impact: "Часть посетителей не может прочитать страницу на телефоне, и оценка доступности падает.",
276
+ },
277
+ "STR-8": {
278
+ title: "<html lang> совпадает с языком текста",
279
+ todo: "Укажите в <html lang> настоящий язык страницы",
280
+ fix: "Указывайте язык, на котором написана страница: `<html lang=\"ru\">` для русской. С неверным языком страница уходит не той аудитории, а браузер предлагает её перевести.",
281
+ impact: "Поисковики могут показывать страницу не той аудитории, а браузер предлагает её перевести.",
282
+ },
283
+ "STR-9": {
284
+ title: "Основное содержимое в <main>",
285
+ todo: "Оберните основное содержимое в <main>",
286
+ fix: "Поместите содержимое страницы в `<main>`, отдельно от шапки, меню и подвала. Поисковикам, AI-поиску и экранным дикторам так проще найти главный текст.",
287
+ impact: "Краулеры и экранные дикторы хуже отличают содержимое от меню.",
288
+ },
289
+ "STR-10": {
290
+ title: "Страница начинается с <!doctype html>",
291
+ todo: "Добавьте <!doctype html> первой строкой",
292
+ fix: "Начинайте каждую страницу с `<!doctype html>`. Без него браузеры включают режим совместимости (quirks mode), и вёрстка может ломаться по-разному в разных браузерах.",
293
+ impact: "Страница может выглядеть по-разному в разных браузерах, а в некоторых — сломанной.",
294
+ },
97
295
  "LNK-1": {
98
296
  title: "Внутренние ссылки работают",
297
+ todo: "Почините битые внутренние ссылки",
99
298
  fix: "Почините или уберите ссылки, которые ведут на ошибки. Битые ссылки тратят обход и теряют вес, который могли бы передать.",
100
299
  impact: "Ссылки ведут на сломанные страницы: посетители уходят, а поисковики обходят сайт хуже.",
101
300
  },
102
301
  "LNK-2": {
103
302
  title: "Внутренние ссылки не идут через редиректы",
303
+ todo: "Ведите ссылки сразу на конечный адрес",
104
304
  fix: "Ссылайтесь сразу на конечный адрес (с правильным слешем, доменом и https), а не на тот, что редиректит.",
105
305
  impact: "Каждый редирект в ссылке замедляет обход и немного снижает её вес.",
106
306
  },
307
+ "LNK-3": {
308
+ title: "На каждую страницу ведёт ссылка",
309
+ todo: "Поставьте ссылки на страницы-сироты со связанных страниц",
310
+ fix: "Добавьте ссылки на эти страницы из меню, списков или похожих страниц. Страницу, которую знает только sitemap, обходят редко, и она не получает вес от остального сайта. Если она не нужна, уберите её из sitemap.",
311
+ impact: "Страницы, на которые никто не ссылается, обходят редко, и они слабо ранжируются.",
312
+ },
313
+ "LNK-4": {
314
+ title: "По ссылкам может перейти краулер",
315
+ todo: "Дайте каждой ссылке настоящий href",
316
+ fix: "Для переходов используйте `<a href=\"/page\">`, а не `<a>` без href, `href=\"#\"` или `javascript:` с обработчиком клика. Краулеры переходят только по href; для действий, которые не ведут на страницу, используйте `<button>`.",
317
+ impact: "Краулеры не могут перейти по этим ссылкам, и страницы за ними находятся позже или никогда.",
318
+ },
319
+ "LNK-5": {
320
+ title: "На внутренних ссылках нет nofollow",
321
+ todo: "Уберите rel=\"nofollow\" со ссылок на свои страницы",
322
+ fix: "Оставьте `nofollow` для платных ссылок и ссылок, за которые вы не ручаетесь. На ссылках на свои страницы он мешает весу переходить внутри сайта и выключает эти страницы из обхода.",
323
+ impact: "Ваши страницы получают меньше веса, и их реже обходят.",
324
+ },
325
+ "LNK-6": {
326
+ title: "Текст ссылки говорит, куда она ведёт",
327
+ todo: "Пишите в тексте ссылки, что на странице, куда она ведёт",
328
+ fix: "Замените «подробнее», «здесь» и пустые ссылки-картинки на текст о целевой странице: «Цены для команд», «Как мы тестируем». По тексту ссылок поисковики понимают, о чём целевая страница, а экранные дикторы читают его отдельно от текста вокруг.",
329
+ impact: "Поисковики меньше узнают о целевых страницах, а пользователи экранных дикторов слышат «ссылка, ссылка».",
330
+ },
331
+ "LNK-7": {
332
+ title: "Ссылки на другие сайты работают",
333
+ todo: "Исправьте или уберите мёртвые ссылки на другие сайты",
334
+ fix: "Обновите ссылки на удалённые страницы и сайты, которых больше нет, или дайте ссылку на копию в архиве. Мёртвые внешние ссылки делают страницу заброшенной на вид.",
335
+ impact: "Посетители упираются в тупик, а страница выглядит заброшенной для людей и поисковиков.",
336
+ },
107
337
  "IMG-1": {
108
338
  title: "У картинок есть alt",
339
+ todo: "Добавьте картинкам alt",
109
340
  fix: 'Добавьте `alt` с тем, что изображено; для декоративных картинок — `alt=""`. От этого зависят поиск по картинкам и экранные дикторы.',
110
341
  impact: "Картинки без alt не видны поиску по картинкам и людям с экранными дикторами.",
111
342
  },
343
+ "IMG-2": {
344
+ title: "У картинок есть width и height",
345
+ todo: "Укажите картинкам width и height",
346
+ fix: "Задайте каждому `<img>` атрибуты `width` и `height` (настоящий размер; CSS всё равно может его менять). Браузер заранее оставит место, и вёрстка не будет прыгать при загрузке (CLS, часть Core Web Vitals).",
347
+ impact: "Страница прыгает при загрузке картинок: люди промахиваются мимо кнопок, и Core Web Vitals ухудшаются.",
348
+ },
349
+ "IMG-3": {
350
+ title: "Ленивая загрузка там, где она помогает",
351
+ todo: "Загружайте первую картинку сразу, а остальные лениво",
352
+ fix: "Не ставьте `loading=\"lazy\"` на первую картинку, обычно самую крупную на первом экране (LCP); ставьте его картинкам ниже на длинной странице, чтобы они не мешали загрузке видимого.",
353
+ impact: "Главная картинка появляется позже, или первый экран ждёт картинки, которых ещё никто не видит.",
354
+ },
355
+ "IMG-4": {
356
+ title: "Фотографии в современных форматах",
357
+ todo: "Отдавайте фотографии в WebP или AVIF",
358
+ fix: "Отдавайте WebP или AVIF: они на 25–50% легче JPEG и PNG при том же качестве. Через `<picture>` с `<source type=\"image/avif\">`, CDN для картинок или компонент картинок вашего фреймворка.",
359
+ impact: "Страницы тяжелее, чем нужно, и медленнее загружаются на телефонах.",
360
+ },
361
+ "IMG-5": {
362
+ title: "Картинки открываются",
363
+ todo: "Почините или уберите битые картинки",
364
+ fix: "Исправьте адрес картинок, которые отвечают ошибкой, или уберите их. Битая картинка оставляет дыру на странице и выпадает из поиска по картинкам.",
365
+ impact: "Посетители видят пустые рамки, а картинки выпадают из поиска по картинкам.",
366
+ },
367
+ "IMG-6": {
368
+ title: "Картинки не тяжелее, чем нужно",
369
+ todo: "Сожмите тяжёлые картинки",
370
+ fix: "Держите картинки легче 500 КБ: уменьшите до размера, в котором их показывают, сожмите и отдавайте телефонам версии поменьше через `srcset`.",
371
+ impact: "Тяжёлые картинки замедляют страницу, особенно на телефонах, и LCP ухудшается.",
372
+ },
373
+ "IMG-7": {
374
+ title: "Alt описывает картинку",
375
+ todo: "Напишите в alt, что изображено",
376
+ fix: "Опишите, что на картинке, коротким предложением: «Команда у доски с планом», а не «IMG_1234.jpg», «фото» или целый абзац. Для декоративных картинок — `alt=\"\"`.",
377
+ impact: "Поиск по картинкам и экранные дикторы получают имя файла вместо описания.",
378
+ },
112
379
  "DATA-1": {
113
380
  title: "В структурированных данных нет синтаксических ошибок",
381
+ todo: "Исправьте синтаксис JSON-LD",
114
382
  fix: "Исправьте блоки JSON-LD, которые не разбираются: одна ошибка отключает весь блок, а с ним и расширенные сниппеты.",
115
383
  impact: "Сломанная разметка игнорируется, и страницы теряют расширенные сниппеты в выдаче.",
116
384
  },
385
+ "DATA-2": {
386
+ title: "У каждого блока есть @context, у каждой сущности — @type",
387
+ todo: "Добавьте в JSON-LD @context и @type",
388
+ fix: "Начинайте каждый блок JSON-LD с `\"@context\": \"https://schema.org\"` и указывайте каждой сущности `@type` (Person, Product, Event…). Без них поисковик не понимает, какой словарь используется, и пропускает разметку.",
389
+ impact: "Поисковики пропускают разметку, и у страницы нет расширенного сниппета.",
390
+ },
391
+ "DATA-3": {
392
+ title: "У типов для расширенных сниппетов есть обязательные поля",
393
+ todo: "Добавьте в разметку обязательные поля",
394
+ fix: "Заполните то, что Google требует для расширенного сниппета каждого типа: Product — цену, отзыв или рейтинг; Event — startDate и location; элементу BreadcrumbList — position. Проверьте в Rich Results Test.",
395
+ impact: "Без обязательных полей Google показывает обычный результат вместо расширенного.",
396
+ },
397
+ "DATA-4": {
398
+ title: "У вложенных страниц есть хлебные крошки",
399
+ todo: "Добавьте BreadcrumbList вложенным страницам",
400
+ fix: "Опишите путь к странице в JSON-LD BreadcrumbList: Главная › Блог › Статья. Тогда Google покажет под заголовком путь, а не голый адрес.",
401
+ impact: "В выдаче голый адрес вместо понятного пути.",
402
+ },
403
+ "DATA-5": {
404
+ title: "Адреса и даты в разметке понятны поисковику",
405
+ todo: "Пишите в разметке полные адреса и даты ISO",
406
+ fix: "Пишите адреса полностью (`https://example.com/logo.png`, а не `/logo.png`), а даты — в ISO 8601 (`2026-11-09` или `2026-11-09T10:00:00+03:00`, а не `09.11.2026`).",
407
+ impact: "Поисковик не может использовать картинку, ссылку или дату, и расширенный сниппет их теряет.",
408
+ },
409
+ "DATA-6": {
410
+ title: "Главная сообщает имя сайта через WebSite",
411
+ todo: "Добавьте на главную WebSite с названием сайта",
412
+ fix: "Поставьте на главную JSON-LD `{\"@type\": \"WebSite\", \"name\": \"…\", \"url\": \"https://example.com/\"}`. Из него Google берёт название сайта, которое показывает над результатами.",
413
+ impact: "Google угадывает название сайта над вашими результатами и может ошибиться.",
414
+ },
117
415
  "LANG-1": {
118
416
  title: "Коды hreflang правильные",
417
+ todo: "Исправьте коды hreflang",
119
418
  fix: "Используйте коды вида `ru`, `en`, `en-GB` или `x-default`. Из-за неверного кода поисковик игнорирует весь набор.",
120
419
  impact: "Неверные коды языков игнорируются, и в выдаче люди получают не ту языковую версию.",
121
420
  },
122
421
  "LANG-2": {
123
422
  title: "Языковые версии ссылаются друг на друга",
423
+ todo: "Сделайте ссылки hreflang взаимными",
124
424
  fix: "Если страница A указывает страницу B как перевод, B должна указывать A. Односторонние hreflang игнорируются.",
125
425
  impact: "Односторонние языковые ссылки игнорируются, и поиск показывает не ту языковую версию.",
126
426
  },
127
427
  "LANG-3": {
128
428
  title: "Есть x-default для остальных языков",
429
+ todo: "Добавьте hreflang x-default",
129
430
  fix: 'Добавьте `<link rel="alternate" hreflang="x-default" href="…">` на версию для всех остальных.',
130
431
  impact: "Посетители с языком, которого нет на сайте, получают случайную версию.",
131
432
  },
433
+ "LANG-4": {
434
+ title: "Каждая версия указывает в hreflang и себя",
435
+ todo: "Добавьте страницу в её собственный список hreflang",
436
+ fix: "Перечисляйте на каждой языковой версии все версии, включая её саму: на русской странице рядом с остальными стоит `hreflang=\"ru\"` на её же адрес.",
437
+ impact: "Поисковики могут не учитывать набор языков и показывать не ту версию.",
438
+ },
439
+ "LANG-5": {
440
+ title: "hreflang ведёт на рабочие канонические страницы",
441
+ todo: "Указывайте в hreflang конечный канонический адрес каждой версии",
442
+ fix: "Ссылайтесь на языковую версию по адресу, который отвечает 200 и указывает canonical на себя: не на редирект, не на ошибку и не на копию с canonical на другую страницу.",
443
+ impact: "Поисковики не учитывают языковые ссылки на сломанные, редиректящие или неканонические страницы.",
444
+ },
445
+ "LANG-6": {
446
+ title: "Язык в hreflang совпадает с языком страницы",
447
+ todo: "Исправьте коды языков или адреса в hreflang",
448
+ fix: "Код должен совпадать со страницей: `hreflang=\"ru\"` ведёт на страницу с `<html lang=\"ru\">`. Обычно перепутаны два кода или шаблон ведёт все языки на одну страницу.",
449
+ impact: "Людей, которые ищут на одном языке, отправляют на страницу на другом.",
450
+ },
451
+ "LANG-7": {
452
+ title: "Каждый язык указан один раз",
453
+ todo: "Оставьте в hreflang один адрес на язык",
454
+ fix: "Давайте каждому коду языка один адрес. Два адреса для `en` обычно значат, что региональной версии нужен свой код: `en-GB` и `en-US`.",
455
+ impact: "Поисковик не понимает, какой адрес показывать для этого языка, и может не учитывать оба.",
456
+ },
457
+ "LANG-8": {
458
+ title: "Адреса в hreflang полные",
459
+ todo: "Пишите в hreflang полные адреса",
460
+ fix: "Указывайте адреса с протоколом и доменом: `https://example.com/ru/`, а не `/ru/`. Относительные адреса в hreflang Google не принимает.",
461
+ impact: "Google не учитывает относительные языковые ссылки.",
462
+ },
132
463
  "SEC-1": {
133
464
  title: "Сайт работает по HTTPS, а http:// на него редиректит",
465
+ todo: "Переведите сайт на HTTPS с редиректом с http://",
134
466
  fix: "Отдавайте сайт по https и перенаправляйте каждый адрес http:// на его https:// версию с кодом 301.",
135
467
  impact: "Без HTTPS браузеры помечают сайт как небезопасный, а поисковики предпочитают защищённых конкурентов.",
136
468
  },
137
469
  "SEC-2": {
138
470
  title: "HTTPS-страницы ничего не грузят по http://",
471
+ todo: "Загружайте все ресурсы по https",
139
472
  fix: "Загружайте картинки, скрипты и стили по https. Браузеры блокируют смешанный контент или предупреждают о нём.",
140
473
  impact: "Смешанный контент ломает части страницы в браузерах, и сайт выглядит небезопасным.",
141
474
  },
475
+ "SEC-3": {
476
+ title: "http:// постоянно редиректит на https://",
477
+ todo: "Сделайте редирект с http:// постоянным (301)",
478
+ fix: "Отвечайте на http:// кодом 301 (или 308) на https://-адрес. 302 и 307 значат «временно»: поисковики оставляют http://-версию и не передают её вес на https://.",
479
+ impact: "Поисковики держат старый http://-адрес, и https://-версия не получает его вес.",
480
+ },
481
+ "SEC-4": {
482
+ title: "Сертификат действителен и не истекает",
483
+ todo: "Обновите TLS-сертификат и включите автопродление",
484
+ fix: "Поставьте сертификат, которому доверяют браузеры, выданный на ваш домен и не просроченный, и продлевайте его автоматически (Let's Encrypt с certbot или сертификат CDN). Сайт с просроченным или чужим сертификатом браузеры блокируют.",
485
+ impact: "Браузеры показывают вместо сайта предупреждение на весь экран, и посетители с краулерами уходят.",
486
+ },
487
+ "SEC-5": {
488
+ title: "Браузерам велено ходить только по https (HSTS)",
489
+ todo: "Добавьте заголовок Strict-Transport-Security",
490
+ fix: "Отдавайте на https-ответах `Strict-Transport-Security: max-age=31536000; includeSubDomains`. Тогда браузер сразу идёт на https:// без редиректа и без момента, когда соединение можно перехватить.",
491
+ impact: "Каждый первый заход идёт через http://: это медленнее и его можно перехватить.",
492
+ },
493
+ "SEC-6": {
494
+ title: "Адреса сайта указаны с https://",
495
+ todo: "Замените http:// на https:// в ссылках, canonical, sitemap и og-тегах",
496
+ fix: "Пишите все адреса сайта с https://: внутренние ссылки, canonical, hreflang, og:url и og:image, sitemap. Каждый http://-адрес стоит лишнего редиректа, а canonical на него называет не ту версию.",
497
+ impact: "Поисковики и посетители ходят через лишние редиректы, а canonical указывает на незащищённую версию.",
498
+ },
499
+ "SEC-7": {
500
+ title: "Есть базовые заголовки безопасности",
501
+ todo: "Добавьте заголовки nosniff, защиту от встраивания и Referrer-Policy",
502
+ fix: "Отдавайте `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN` (или CSP `frame-ancestors 'self'`) и `Referrer-Policy: strict-origin-when-cross-origin`. На позиции они не влияют, но защищают посетителей от частых атак.",
503
+ impact: "Сайт проще атаковать: подмена типа файла, встраивание в чужую страницу, утечка адресов.",
504
+ },
142
505
  },
143
506
  m: {
507
+ robotsStatus: (status) => `robots.txt отвечает ${status}: пока он не ответит, Google считает весь сайт закрытым`,
508
+ robotsHtml: "robots.txt отдаёт HTML-страницу вместо текстовых правил",
509
+ robotsSize: (kb) => `robots.txt весит ${kb} КБ: Google читает только первые 500 КБ`,
510
+ robotsBadLines: (n, line) => `непонятных поисковикам строк: ${n}, например «${line}»`,
511
+ blockedPage: (bots) => `robots.txt закрывает её для ${bots}`,
512
+ blockedBySitemap: (bots) => `есть в sitemap, но robots.txt закрывает её для ${bots}`,
513
+ otherHostAnswers: (url, status) => `${url} отвечает ${status} вместо редиректа на основной адрес`,
514
+ slashDuplicate: (url) => `${url} тоже отвечает 200, без редиректа и без canonical на эту страницу`,
515
+ redirectChain: (url, n, path) => `${url}: редиректов подряд — ${n}: ${path}`,
516
+ redirectLoop: (url, path) => `${url} уходит в петлю редиректов: ${path}`,
517
+ noindexHome: "главная закрыта noindex: сайт выпадает из поиска",
518
+ noindexInSitemap: "есть в sitemap, но закрыта noindex",
519
+ noindexWithCanonical: (href) => `noindex вместе с canonical на ${href}: два разных сигнала`,
520
+ canonicalToNoindex: (href) => `canonical ведёт на ${href}, а она закрыта noindex`,
521
+ blockedResource: (url) => `robots.txt закрывает ${url} для Googlebot`,
522
+ slowServer: (ms, limit) => `главная начинает приходить через ${(ms / 1000).toFixed(1).replace(".", ",")} с (медиана из 3 запросов), нужно до ${String(limit / 1000).replace(".", ",")} с`,
523
+ skipNoRobots: "нет robots.txt",
524
+ skipNoSlashPage: "среди проверенных нет страниц, кроме главной",
525
+ temporaryRedirect: (status) => `отвечает ${status}, временным редиректом, вместо 301`,
526
+ certExpired: (date) => `сертификат истёк ${date}`,
527
+ certInvalid: (error) => `браузеры не доверяют сертификату: ${error}`,
528
+ certExpiring: (days, date) => `сертификат истекает через ${days} дн., ${date}`,
529
+ noHsts: "нет заголовка Strict-Transport-Security",
530
+ shortHsts: (days) => `max-age в HSTS — ${days} дн.; нужно не меньше 180`,
531
+ httpAddress: (where, url) => `${where} с http://: ${url}`,
532
+ httpLinks: (n, url) => `внутренних ссылок с http://: ${n}, например ${url}`,
533
+ httpInSitemap: (n, url) => `адресов с http://: ${n}, например ${url}`,
534
+ missingHeaders: (names) => `нет: ${names}`,
535
+ manyCanonicals: (n, hrefs) => `разных canonical на странице: ${n} — ${hrefs}`,
536
+ canonicalInBody: (href) => `canonical ${href} стоит в <body>`,
537
+ canonicalConflict: (html, header) => `в HTML указан ${html}, в заголовке Link — ${header}`,
538
+ canonicalChain: (target, next) => `canonical ведёт на ${target}, а её canonical — ${next}`,
539
+ utmNoCanonical: (url) => `у ${url} нет canonical`,
540
+ utmCanonical: (url) => `${url} указывает canonical на себя вместе с UTM-метками`,
541
+ canonicalOtherLanguage: (href, to, from) => `canonical ведёт на ${href} — версию «${to}» для страницы «${from}»`,
542
+ sitemapStatus: (status) => `отвечает ${status}`,
543
+ sitemapHtml: "отдаёт HTML-страницу вместо XML",
544
+ sitemapNotXml: "нет ни <urlset>, ни <sitemapindex>",
545
+ sitemapTooMany: (n) => `адресов: ${n}, больше лимита в 50 000 на файл`,
546
+ sitemapTooBig: (mb) => `${mb} МБ, больше лимита в 50 МБ на файл`,
547
+ sitemapAmpersand: (n) => `адресов с неэкранированным &, который ломает XML: ${n}`,
548
+ sitemapForeign: (n, example) => `относительных адресов или адресов другого сайта: ${n}, например ${example}`,
549
+ notInSitemap: (n) => `на неё ссылаются проверенные страницы (${n}), но в sitemap её нет`,
550
+ sitemapDead: (status) => `есть в sitemap, но отвечает ${status}`,
551
+ lastmodBad: (n, example) => `lastmod не в формате W3C: ${n}, например «${example}»`,
552
+ lastmodFuture: (n, example) => `lastmod в будущем: ${n}, например ${example}`,
553
+ lastmodSame: (n, value) => `у всех ${n} адресов одинаковый lastmod, ${value}: похоже на время сборки`,
554
+ skipNoLastmod: "в sitemap нет lastmod",
555
+ manyTitles: (n) => `тегов <title> в <head>: ${n}`,
556
+ manyDescriptions: (n) => `meta description в <head>: ${n}`,
557
+ genericTitle: (title) => `title — «${title}»`,
558
+ descriptionRepeatsTitle: "description повторяет title",
559
+ noCharset: "нет charset ни в заголовке Content-Type, ни в <meta charset>",
560
+ noSnippet: (rule) => `сниппет запрещён: ${rule}`,
561
+ noFavicon: (status) => `в <head> нет иконки, а /favicon.ico отвечает ${status}`,
562
+ emptyH1: "в H1 нет текста: только картинка или ничего",
563
+ emptyHeadings: (n, levels) => `пустых заголовков: ${n} — ${levels}`,
564
+ titleH1Apart: (title, h1) => `ни одного общего слова: title «${title}», H1 «${h1}»`,
565
+ noZoom: (viewport) => `масштабирование запрещено: ${viewport}`,
566
+ langMismatch: (lang, script, percent) => `<html lang="${lang}">, но ${percent}% букв — ${script}`,
567
+ scriptCyrillic: "кириллица",
568
+ scriptLatin: "латиница",
569
+ noMain: "на странице нет <main>",
570
+ noDoctype: "в начале нет <!doctype html>",
571
+ orphan: "на страницу не ведёт ни одна проверенная страница: о ней знает только sitemap",
572
+ uncrawlableLinks: (n, text) => `ссылок без адреса для перехода (нет href, # или javascript:): ${n}, например «${text}»`,
573
+ nofollowInternal: (n, url) => `внутренних ссылок с nofollow: ${n}, например на ${url}`,
574
+ vagueLinks: (n, text) => `ссылок, текст которых ничего не говорит о цели: ${n}, например «${text}»`,
575
+ namelessLinks: (n, href) => `ссылок без текста, подписи и alt: ${n}, например на ${href}`,
576
+ deadExternal: (url, status) => `ссылка на ${url} отвечает ${status}`,
577
+ skipPartialSample: "проверены не все страницы из sitemap: запустите с --pages all",
578
+ skipNoExternal: "на проверенных страницах нет ссылок на другие сайты",
579
+ skipNoNetwork: "ни один внешний сайт не ответил: у проверки нет доступа в интернет",
580
+ imagesNoSize: (n, total, src) => `без width и height ${n} из ${total} картинок, например ${src}`,
581
+ firstImageLazy: (src) => `первая картинка загружается лениво: ${src}`,
582
+ noLazyImages: (n) => `картинок: ${n}, и ни одна не загружается лениво`,
583
+ oldFormats: (n, src) => `картинок только в JPEG или PNG: ${n}, например ${src}`,
584
+ brokenImage: (src, status) => `картинка ${src} отвечает ${status}`,
585
+ heavyImage: (src, kb, max) => `картинка ${src} весит ${kb} КБ, нужно до ${max} КБ`,
586
+ poorAlt: (n, alt) => `alt, который не описывает картинку: ${n}, например «${alt}»`,
587
+ noContext: "блок JSON-LD без @context schema.org",
588
+ noType: (n) => `сущностей без @type: ${n}`,
589
+ missingProperties: (type, keys) => `${type} без ${keys}`,
590
+ noBreadcrumbs: "нет BreadcrumbList",
591
+ relativeInMarkup: (n, example) => `неполных адресов: ${n}, например ${example}`,
592
+ badDatesInMarkup: (n, example) => `дат не в ISO 8601: ${n}, например ${example}`,
593
+ noWebSite: "на главной нет WebSite с названием",
594
+ skipNoRichTypes: "на проверенных страницах нет типов с расширенными сниппетами",
595
+ skipNoNestedPages: "среди проверенных нет вложенных страниц",
596
+ noSelfHreflang: "страницы нет в её собственном списке hreflang",
597
+ hreflangStatus: (lang, url, status) => `«${lang}» ведёт на ${url}, а та отвечает ${status}`,
598
+ hreflangRedirects: (lang, url, to) => `«${lang}» ведёт на ${url}, а та редиректит на ${to}`,
599
+ hreflangNotCanonical: (lang, url, canonical) => `«${lang}» ведёт на ${url}, а её canonical — ${canonical}`,
600
+ hreflangLangMismatch: (lang, url, htmlLang) => `«${lang}» ведёт на ${url} — страницу с lang="${htmlLang}"`,
601
+ duplicateHreflang: (codes) => `больше одного адреса для ${codes}`,
602
+ relativeHreflang: (n, href) => `относительных адресов: ${n}, например ${href}`,
144
603
  status: (status) => `отвечает ${status}`,
145
604
  soft404Probe: (url) => `выдуманный адрес ${url} отвечает 200 вместо 404`,
146
605
  soft404Page: (title) => `отвечает 200, но похожа на страницу «не найдено»: «${title}»`,
@@ -154,18 +613,18 @@ export const ru = {
154
613
  sitemapRedirects: (to) => `есть в sitemap, редиректит на ${to}`,
155
614
  sitemapCanonicalElsewhere: (href) => `есть в sitemap, но её canonical — ${href}`,
156
615
  noTitle: "нет <title>",
157
- titleLength: (n, title) => `title на ${n} символов: «${title}»`,
616
+ titleLength: (n, title, min, max) => `title на ${n} ${chars(n)}, нужно ${min}–${max}: «${title}»`,
158
617
  noDescription: "нет meta description",
159
- descriptionLength: (n) => `description на ${n} символов`,
160
- sameTitle: (n, title) => `одинаковый title на ${n} страницах: «${title}»`,
161
- sameDescription: (n, text) => `одинаковый description на ${n} страницах: «${text}»`,
618
+ descriptionLength: (n, min, max) => `description на ${n} ${chars(n)}, нужно ${min}–${max}`,
619
+ sameTitle: (n, title) => `одинаковый title на ${n} страницах, у каждой должен быть свой: «${title}»`,
620
+ sameDescription: (n, text) => `одинаковый description на ${n} страницах, у каждой должен быть свой: «${text}»`,
162
621
  noH1: "нет H1",
163
- manyH1: (n) => `заголовков H1: ${n}`,
164
- skippedLevel: (from, to, text) => `после H${from} идёт H${to}: «${text}»`,
622
+ manyH1: (n) => `заголовков H1: ${n}, нужен один`,
623
+ skippedLevel: (from, to, text) => `после H${from} идёт H${to}, уровень пропущен: «${text}»`,
165
624
  noLang: "нет <html lang>",
166
625
  noViewport: "нет meta viewport",
167
- brokenLink: (status, from) => `отвечает ${status}, ссылка со страницы ${from}`,
168
- redirectLink: (to, from) => `редиректит на ${to}, ссылка со страницы ${from}`,
626
+ brokenLink: (url, status) => `ссылка на ${url} отвечает ${status}`,
627
+ redirectLink: (url, to) => `ссылка на ${url} редиректит на ${to}`,
169
628
  linksNotChecked: (checked, found) => `проверено внутренних ссылок: ${checked} из ${found}; чтобы проверить больше, увеличьте --max-links`,
170
629
  imagesNoAlt: (n, total, example) => `без alt ${n} из ${total} картинок, например ${example}`,
171
630
  jsonLdError: (error) => `JSON-LD не разбирается: ${error}`,
@@ -180,6 +639,7 @@ export const ru = {
180
639
  skipNoHreflang: "на сайте нет hreflang",
181
640
  skipOneLanguage: "на сайте один язык",
182
641
  skipNoImages: "на проверенных страницах нет картинок",
642
+ skipNoOwnImages: "на проверенных страницах нет картинок этого сайта: чужие не запрашиваются",
183
643
  skipNoJsonLd: "на проверенных страницах нет структурированных данных",
184
644
  skipNoLinks: "на проверенных страницах нет внутренних ссылок",
185
645
  skipNotHttps: "сайт не на https",