dsh-desktop-kit 0.0.0-stage → 2.0.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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -2
  3. package/client.js +4101 -0
  4. package/cordis.patch.yml +5 -0
  5. package/docs/ARCHITECTURE.md +114 -0
  6. package/docs/COMPATIBILITY.md +61 -0
  7. package/docs/INSTALL.md +84 -0
  8. package/docs/PRIVACY.md +50 -0
  9. package/docs/ROADMAP.md +57 -0
  10. package/docs/TOKENS.md +118 -0
  11. package/docs/TROUBLESHOOTING.md +68 -0
  12. package/docs/images/hero.png +0 -0
  13. package/index.js +84 -0
  14. package/package.json +70 -4
  15. package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
  16. package/plugins/dsh-gkb-ru-ui/lib/client.js +1331 -0
  17. package/plugins/dsh-gkb-ru-ui/lib/index.js +186 -0
  18. package/plugins/dsh-gkb-ru-ui/lib/skills.js +570 -0
  19. package/plugins/dsh-gkb-ru-ui/package.json +28 -0
  20. package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
  21. package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
  22. package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
  23. package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
  24. package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
  25. package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
  26. package/plugins/dsh-peak-guard/README.md +70 -0
  27. package/plugins/dsh-peak-guard/client.js +741 -0
  28. package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
  29. package/plugins/dsh-peak-guard/icon.svg +12 -0
  30. package/plugins/dsh-peak-guard/index.js +2 -0
  31. package/plugins/dsh-peak-guard/locale/en.json +6 -0
  32. package/plugins/dsh-peak-guard/locale/ru.json +6 -0
  33. package/plugins/dsh-peak-guard/locale/zh.json +6 -0
  34. package/plugins/dsh-peak-guard/package.json +33 -0
  35. package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
  36. package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
  37. package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
  38. package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
  39. package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
  40. package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
  41. package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
  42. package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
  43. package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
  44. package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
  45. package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
  46. package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
  47. package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
  48. package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
  49. package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
  50. package/plugins/dsh-profile-transfer/README.md +62 -0
  51. package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
  52. package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
  53. package/plugins/dsh-profile-transfer/bootstrap/README-/320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/232/320/220.md +42 -0
  54. package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
  55. package/plugins/dsh-profile-transfer/bootstrap//320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/230/320/242/320/254.cmd +5 -0
  56. package/plugins/dsh-profile-transfer/client.js +806 -0
  57. package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
  58. package/plugins/dsh-profile-transfer/icon.svg +13 -0
  59. package/plugins/dsh-profile-transfer/index.js +375 -0
  60. package/plugins/dsh-profile-transfer/locale/en.json +6 -0
  61. package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
  62. package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
  63. package/plugins/dsh-profile-transfer/package.json +36 -0
  64. package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
  65. package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
  66. package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
  67. package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
  68. package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
  69. package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
  70. package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
  71. package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
  72. package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
  73. package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
  74. package/plugins/dsh-profile-transfer/transfer.js +1004 -0
  75. package/plugins/dsh-project-memory/README.md +71 -0
  76. package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
  77. package/plugins/dsh-project-memory/icon.svg +12 -0
  78. package/plugins/dsh-project-memory/index.js +1240 -0
  79. package/plugins/dsh-project-memory/locale/en.json +6 -0
  80. package/plugins/dsh-project-memory/locale/ru.json +6 -0
  81. package/plugins/dsh-project-memory/locale/zh.json +6 -0
  82. package/plugins/dsh-project-memory/package.json +26 -0
  83. package/plugins/dsh-session-move/README.md +67 -0
  84. package/plugins/dsh-session-move/client.js +719 -0
  85. package/plugins/dsh-session-move/cordis.patch.yml +5 -0
  86. package/plugins/dsh-session-move/icon.svg +14 -0
  87. package/plugins/dsh-session-move/index.js +497 -0
  88. package/plugins/dsh-session-move/locale/en.json +6 -0
  89. package/plugins/dsh-session-move/locale/ru.json +6 -0
  90. package/plugins/dsh-session-move/locale/zh.json +6 -0
  91. package/plugins/dsh-session-move/package.json +31 -0
  92. package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
  93. package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
  94. package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
  95. package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
  96. package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
  97. package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
  98. package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
  99. package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
  100. package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
  101. package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
  102. package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
  103. package/plugins/dsh-skill-autopilot/index.js +444 -0
  104. package/plugins/dsh-skill-autopilot/package.json +19 -0
  105. package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
  106. package/plugins/dsh-token-lab/lib/client.js +504 -0
  107. package/plugins/dsh-token-lab/lib/index.js +275 -0
  108. package/plugins/dsh-token-lab/package.json +26 -0
  109. package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
  110. package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
  111. package/skills/code-review-checklist/SKILL.md +55 -0
  112. package/skills/content-writing/README.md +87 -0
  113. package/skills/content-writing/SKILL.md +85 -0
  114. package/skills/content-writing/assets/checklists/fact-check.md +53 -0
  115. package/skills/content-writing/assets/checklists/publish.md +65 -0
  116. package/skills/content-writing/assets/checklists/social.md +40 -0
  117. package/skills/content-writing/assets/templates/article.md +47 -0
  118. package/skills/content-writing/assets/templates/brief.md +44 -0
  119. package/skills/content-writing/assets/templates/doctor-card.md +41 -0
  120. package/skills/content-writing/assets/templates/image-prompts.md +128 -0
  121. package/skills/content-writing/assets/templates/post-telegram.md +36 -0
  122. package/skills/content-writing/assets/templates/post-vk.md +42 -0
  123. package/skills/content-writing/assets/templates/press-release.md +56 -0
  124. package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
  125. package/skills/content-writing/assets/templates/service-card.md +46 -0
  126. package/skills/content-writing/references/anti-ai-style.md +248 -0
  127. package/skills/content-writing/references/editing.md +185 -0
  128. package/skills/content-writing/references/fact-checking.md +205 -0
  129. package/skills/content-writing/references/government-medical.md +246 -0
  130. package/skills/content-writing/references/images.md +212 -0
  131. package/skills/content-writing/references/journalism.md +252 -0
  132. package/skills/content-writing/references/process.md +123 -0
  133. package/skills/content-writing/references/quick-recipes.md +99 -0
  134. package/skills/content-writing/references/rules.md +85 -0
  135. package/skills/content-writing/references/science-popular.md +218 -0
  136. package/skills/content-writing/references/seo-content.md +172 -0
  137. package/skills/content-writing/references/social-media.md +253 -0
  138. package/skills/content-writing/references/sources.md +49 -0
  139. package/skills/content-writing/references/templates.md +291 -0
  140. package/skills/context-economy/SKILL.md +60 -0
  141. package/skills/context-economy/references/long-session.md +35 -0
  142. package/skills/context-economy/references/token-math.md +42 -0
  143. package/skills/context-economy/references/tool-recipes.md +34 -0
  144. package/skills/database-safety/SKILL.md +47 -0
  145. package/skills/debugging-playbook/SKILL.md +42 -0
  146. package/skills/design-critique/SKILL.md +65 -0
  147. package/skills/design-critique/references/a11y.md +25 -0
  148. package/skills/design-critique/references/diagnoses.md +33 -0
  149. package/skills/design-critique/references/quick-tests.md +30 -0
  150. package/skills/design-critique/references/report-template.md +63 -0
  151. package/skills/design-critique/references/rubric-fast.md +73 -0
  152. package/skills/dev-workflow/SKILL.md +48 -0
  153. package/skills/dev-workflow/references/definition-of-done.md +44 -0
  154. package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
  155. package/skills/dev-workflow/references/stages.md +67 -0
  156. package/skills/git-and-ci/SKILL.md +60 -0
  157. package/skills/pg-upgrade-verify/SKILL.md +67 -0
  158. package/skills/project-deep-dive/SKILL.md +45 -0
  159. package/skills/project-deep-dive/references/recon-commands.md +90 -0
  160. package/skills/project-deep-dive/references/section-templates.md +104 -0
  161. package/skills/project-deep-dive/references/tool-and-process.md +86 -0
  162. package/skills/project-memory-maintenance/SKILL.md +55 -0
  163. package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
  164. package/skills/project-memory-maintenance/references/journal-template.md +50 -0
  165. package/skills/project-memory-maintenance/references/section-rules.md +59 -0
  166. package/skills/release-and-deploy/SKILL.md +53 -0
  167. package/skills/requirements-and-planning/SKILL.md +49 -0
  168. package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
  169. package/skills/requirements-and-planning/references/process-steps.md +52 -0
  170. package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
  171. package/skills/security-review/SKILL.md +40 -0
  172. package/skills/seo/README.md +60 -0
  173. package/skills/seo/SKILL.md +77 -0
  174. package/skills/seo/assets/README.md +66 -0
  175. package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
  176. package/skills/seo/assets/scripts/README.md +179 -0
  177. package/skills/seo/assets/scripts/lib/args.mjs +105 -0
  178. package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
  179. package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
  180. package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
  181. package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
  182. package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
  183. package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
  184. package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
  185. package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
  186. package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
  187. package/skills/seo/assets/snippets/meta.blade.php +61 -0
  188. package/skills/seo/assets/snippets/robots.txt +61 -0
  189. package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
  190. package/skills/seo/assets/snippets/sitemap.xml +36 -0
  191. package/skills/seo/references/antipatterns.md +66 -0
  192. package/skills/seo/references/audit-checklist.md +319 -0
  193. package/skills/seo/references/content-seo.md +204 -0
  194. package/skills/seo/references/eeat-medical.md +219 -0
  195. package/skills/seo/references/measurement.md +250 -0
  196. package/skills/seo/references/on-page.md +214 -0
  197. package/skills/seo/references/page-templates.md +56 -0
  198. package/skills/seo/references/stack-recipes.md +695 -0
  199. package/skills/seo/references/structured-data.md +487 -0
  200. package/skills/seo/references/technical-seo.md +296 -0
  201. package/skills/service-desk-conventions/SKILL.md +52 -0
  202. package/skills/service-desk-db-safety/SKILL.md +63 -0
  203. package/skills/service-desk-release/SKILL.md +61 -0
  204. package/skills/service-desk-testing/SKILL.md +61 -0
  205. package/skills/service-desk-ui/SKILL.md +52 -0
  206. package/skills/skills-router/SKILL.md +53 -0
  207. package/skills/skills-router/references/applying-skills.md +25 -0
  208. package/skills/skills-router/references/routing-matrix.md +52 -0
  209. package/skills/testing-strategy/SKILL.md +52 -0
  210. package/skills/ui-design-system/SKILL.md +50 -0
  211. package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
  212. package/skills/ui-design-system/references/tokens-and-components.md +67 -0
  213. package/skills/ui-design-system/references/visual-qa.md +44 -0
  214. package/skills/web-research/SKILL.md +47 -0
  215. package/skills/web-research/references/answer-and-citations.md +60 -0
  216. package/skills/web-research/references/query-recipes.md +44 -0
  217. package/skills/wow-design/SKILL.md +89 -0
  218. package/skills/wow-design/assets/kit/README.md +195 -0
  219. package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
  220. package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
  221. package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
  222. package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
  223. package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
  224. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
  225. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
  226. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
  227. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
  228. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
  229. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
  230. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
  231. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
  232. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
  233. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
  234. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
  235. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
  236. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
  237. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
  238. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
  239. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
  240. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
  241. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
  242. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
  243. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
  244. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
  245. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
  246. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
  247. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
  248. package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
  249. package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
  250. package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
  251. package/skills/wow-design/assets/kit/showcase.html +1164 -0
  252. package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
  253. package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
  254. package/skills/wow-design/assets/scripts/README.md +312 -0
  255. package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
  256. package/skills/wow-design/assets/scripts/capture.mjs +337 -0
  257. package/skills/wow-design/assets/scripts/package.json +30 -0
  258. package/skills/wow-design/references/anti-patterns.md +252 -0
  259. package/skills/wow-design/references/critique-rubric.md +262 -0
  260. package/skills/wow-design/references/design-brief-template.md +48 -0
  261. package/skills/wow-design/references/inspiration.md +203 -0
  262. package/skills/wow-design/references/presets.md +99 -0
  263. package/skills/wow-design/references/principles.md +371 -0
  264. package/skills/wow-design/references/quick-presets.md +50 -0
  265. package/skills/wow-design/references/report-template.md +21 -0
  266. package/skills/wow-design/references/stack-recipes.md +96 -0
  267. package/skills/wow-design/references/visual-qa.md +82 -0
  268. package/skills/wow-design/references/wow-levers.md +43 -0
  269. package/tools/skill-cost.mjs +76 -0
@@ -0,0 +1,695 @@
1
+ > Часть навыка seo. ≈10 100 токенов. Читать, когда: пишешь SEO в коде — Laravel/Blade, Bitrix/Aspro, React/Vite, статика; один источник SEO-данных, мета и JSON-LD в исходном HTML, сервер/деплой.
2
+
3
+ # Стек-рецепты: Laravel/Blade, Bitrix/Aspro, React/Vite, статика
4
+
5
+ > Готовые паттерны, чтобы SEO-по-умолчанию не превращалось в ручную работу. Всё написано так, чтобы
6
+ > **мета и JSON-LD попадали в исходный HTML на сервере** (это главное требование любого стека).
7
+ > Проверено 04.11.2026.
8
+
9
+ ## 0. Общие принципы (для любого стека)
10
+
11
+ 1. **Один источник SEO-данных.** Не разбрасывай title/description по шаблонам: собери объект `SeoData`
12
+ (title, description, canonical, robots, og, jsonld, breadcrumbs) и рендерь его в одном месте.
13
+ 2. **Значения по умолчанию** (сайт/организация/шаблон title) + переопределение на странице.
14
+ 3. **Экранирование.** Всё, что попадает в HTML-атрибуты, — через escape (`{{ }}` в Blade, `htmlspecialchars`
15
+ в PHP, `escapeHtml` в JS). JSON-LD — через `json_encode` с флагами, не склейкой строк.
16
+ 4. **Абсолютные URL** стройте от `url()`/`config('app.url')`/`Request::root()`, а не от `/path` вручную.
17
+ 5. **Проверка в CI:** страница 200, title/description/H1 есть, canonical есть, JSON-LD валиден, 0 critical
18
+ в `seo-audit.mjs`.
19
+ 6. **Не трогай SEO-данные без baseline** (какие title/URL были до). Смена URL — 301 (см. `technical-seo.md`).
20
+
21
+ ---
22
+
23
+ ## 1. Laravel + Blade
24
+
25
+ ### 1.1 Объект SEO-данных
26
+
27
+ ```php
28
+ <?php
29
+ // app/Support/Seo/SeoData.php
30
+ namespace App\Support\Seo;
31
+
32
+ final class SeoData
33
+ {
34
+ public array $jsonLd = [];
35
+ public array $breadcrumbs = [];
36
+ public ?string $ogImage = null;
37
+
38
+ public function __construct(
39
+ public string $title,
40
+ public string $description,
41
+ public string $canonical,
42
+ public string $robots = 'index,follow',
43
+ public string $type = 'website',
44
+ ) {}
45
+
46
+ public function addJsonLd(array $node): self
47
+ {
48
+ $this->jsonLd[] = $node;
49
+
50
+ return $this;
51
+ }
52
+
53
+ public function breadcrumbs(array $items): self
54
+ {
55
+ $this->breadcrumbs = $items; // [['name' => 'Главная', 'url' => '/'], ...]
56
+
57
+ return $this;
58
+ }
59
+
60
+ public function toArray(): array
61
+ {
62
+ return [
63
+ 'title' => $this->title,
64
+ 'description' => $this->description,
65
+ 'canonical' => $this->canonical,
66
+ 'robots' => $this->robots,
67
+ 'type' => $this->type,
68
+ 'ogImage' => $this->ogImage,
69
+ 'jsonLd' => array_merge($this->jsonLd, $this->breadcrumbNode()),
70
+ 'breadcrumbs' => $this->breadcrumbs,
71
+ ];
72
+ }
73
+
74
+ private function breadcrumbNode(): array
75
+ {
76
+ if (count($this->breadcrumbs) < 2) {
77
+ return [];
78
+ }
79
+
80
+ return [[
81
+ '@context' => 'https://schema.org',
82
+ '@type' => 'BreadcrumbList',
83
+ 'itemListElement' => array_map(
84
+ fn (array $item, int $i) => array_filter([
85
+ '@type' => 'ListItem',
86
+ 'position' => $i + 1,
87
+ 'name' => $item['name'],
88
+ 'item' => isset($item['url']) ? url($item['url']) : null,
89
+ ]),
90
+ $this->breadcrumbs,
91
+ array_keys($this->breadcrumbs),
92
+ ),
93
+ ]];
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### 1.2 Глобальные данные + хелпер
99
+
100
+ ```php
101
+ <?php
102
+ // app/Support/Seo/Seo.php
103
+ namespace App\Support\Seo;
104
+
105
+ class Seo
106
+ {
107
+ public static function page(string $title, string $description, ?string $path = null): SeoData
108
+ {
109
+ $canonical = $path ? url($path) : url()->current();
110
+
111
+ return new SeoData(
112
+ title: $title,
113
+ description: $description,
114
+ canonical: $canonical,
115
+ );
116
+ }
117
+
118
+ public static function organization(): array
119
+ {
120
+ return [
121
+ '@context' => 'https://schema.org',
122
+ '@type' => ['MedicalOrganization', 'MedicalClinic'],
123
+ '@id' => url('/') . '#organization',
124
+ 'name' => config('seo.org_name'),
125
+ 'url' => url('/'),
126
+ 'logo' => ['@type' => 'ImageObject', 'url' => asset('img/logo-512.png')],
127
+ 'telephone' => config('seo.org_phone'),
128
+ 'address' => [
129
+ '@type' => 'PostalAddress',
130
+ 'streetAddress' => config('seo.addr_street'),
131
+ 'addressLocality' => config('seo.addr_city'),
132
+ 'postalCode' => config('seo.addr_zip'),
133
+ 'addressCountry' => 'RU',
134
+ ],
135
+ 'sameAs' => config('seo.same_as', []),
136
+ ];
137
+ }
138
+ }
139
+ ```
140
+
141
+ `config/seo.php` — единый конфиг: название, телефон, адрес, соцсети, `og:image` по умолчанию, `robots` для
142
+ закрытых разделов. Значения — из `.env`/админки, **не хардкод** по шаблонам.
143
+
144
+ ### 1.3 Blade-компонент `<x-seo-meta>`
145
+
146
+ ```blade
147
+ {{-- resources/views/components/seo/meta.blade.php --}}
148
+ @props(['seo'])
149
+ @php
150
+ $seo = is_array($seo) ? $seo : $seo->toArray();
151
+ $jsonLd = array_values(array_filter($seo['jsonLd'] ?? []));
152
+ @endphp
153
+
154
+ <title>{{ $seo['title'] }}</title>
155
+ <meta name="description" content="{{ $seo['description'] }}">
156
+ <meta name="robots" content="{{ $seo['robots'] ?? 'index,follow' }}">
157
+ <link rel="canonical" href="{{ $seo['canonical'] }}">
158
+
159
+ {{-- Open Graph --}}
160
+ <meta property="og:type" content="{{ $seo['type'] ?? 'website' }}">
161
+ <meta property="og:site_name" content="{{ config('seo.org_name') }}">
162
+ <meta property="og:locale" content="ru_RU">
163
+ <meta property="og:title" content="{{ $seo['title'] }}">
164
+ <meta property="og:description" content="{{ $seo['description'] }}">
165
+ <meta property="og:url" content="{{ $seo['canonical'] }}">
166
+ <meta property="og:image" content="{{ $seo['ogImage'] ?? asset('img/og-default-1200x630.jpg') }}">
167
+ <meta property="og:image:width" content="1200">
168
+ <meta property="og:image:height" content="630">
169
+ <meta name="twitter:card" content="summary_large_image">
170
+
171
+ @if ($jsonLd)
172
+ @foreach ($jsonLd as $node)
173
+ <script type="application/ld+json">{!! json_encode($node, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP) !!}</script>
174
+ @endforeach
175
+ @endif
176
+ ```
177
+
178
+ Флаги **обязательны**: `JSON_UNESCAPED_UNICODE` (кириллица читаемо), `JSON_UNESCAPED_SLASHES` (URL не
179
+ `\/`), `JSON_HEX_TAG`/`JSON_HEX_AMP` (защита от XSS/разрыва тега).
180
+
181
+ ### 1.4 Layout
182
+
183
+ ```blade
184
+ {{-- resources/views/layouts/app.blade.php --}}
185
+ <!doctype html>
186
+ <html lang="ru" prefix="og: https://ogp.me/ns#">
187
+ <head>
188
+ <meta charset="utf-8">
189
+ <meta name="viewport" content="width=device-width, initial-scale=1">
190
+ <x-seo.meta :seo="$seo ?? \App\Support\Seo\Seo::page(
191
+ config('seo.default_title'),
192
+ config('seo.default_description')
193
+ )" />
194
+ @stack('head')
195
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
196
+ </head>
197
+ <body>
198
+ @include('partials.header')
199
+ <main id="main">
200
+ @yield('content')
201
+ </main>
202
+ @include('partials.footer')
203
+ @stack('scripts')
204
+ </body>
205
+ </html>
206
+ ```
207
+
208
+ ### 1.5 Контроллер страницы услуги
209
+
210
+ ```php
211
+ public function show(Service $service): View
212
+ {
213
+ $seo = Seo::page(
214
+ title: "{$service->title} в организация — цена от {$service->price_from} ₽",
215
+ description: Str::limit($service->short_description, 155),
216
+ path: "/uslugi/{$service->slug}/",
217
+ )
218
+ ->breadcrumbs([
219
+ ['name' => 'Главная', 'url' => '/'],
220
+ ['name' => 'Услуги', 'url' => '/uslugi/'],
221
+ ['name' => $service->category->title, 'url' => "/uslugi/{$service->category->slug}/"],
222
+ ['name' => $service->title],
223
+ ])
224
+ ->addJsonLd([
225
+ '@context' => 'https://schema.org',
226
+ '@type' => 'MedicalWebPage',
227
+ 'url' => url("/uslugi/{$service->slug}/"),
228
+ 'name' => $service->title,
229
+ 'description' => $service->short_description,
230
+ 'inLanguage' => 'ru-RU',
231
+ 'datePublished' => $service->published_at?->toDateString(),
232
+ 'dateModified' => $service->updated_at->toDateString(),
233
+ 'lastReviewed' => $service->reviewed_at?->toDateString(),
234
+ 'reviewedBy' => $service->reviewer ? [
235
+ '@type' => 'Physician',
236
+ 'name' => $service->reviewer->full_name,
237
+ 'jobTitle' => $service->reviewer->specialty,
238
+ 'url' => route('doctors.show', $service->reviewer),
239
+ ] : null,
240
+ ]);
241
+
242
+ return view('services.show', compact('service', 'seo'));
243
+ }
244
+ ```
245
+
246
+ ### 1.6 sitemap.xml и robots.txt (роуты, а не файлы)
247
+
248
+ ```php
249
+ // routes/web.php
250
+ Route::get('/sitemap.xml', SitemapController::class)->name('sitemap');
251
+ Route::get('/robots.txt', RobotsController::class)->name('robots');
252
+ ```
253
+
254
+ ```php
255
+ <?php
256
+ namespace App\Http\Controllers;
257
+
258
+ use Illuminate\Http\Response;
259
+
260
+ class RobotsController extends Controller
261
+ {
262
+ public function __invoke(): Response
263
+ {
264
+ $content = implode("\n", [
265
+ 'User-agent: *',
266
+ 'Disallow: /admin',
267
+ 'Disallow: /login',
268
+ 'Disallow: /search',
269
+ 'Disallow: /*?sort=',
270
+ 'Allow: /',
271
+ '',
272
+ 'Sitemap: ' . url('/sitemap.xml'),
273
+ ]);
274
+
275
+ return response($content, 200, ['Content-Type' => 'text/plain; charset=UTF-8']);
276
+ }
277
+ }
278
+ ```
279
+
280
+ ```php
281
+ <?php
282
+ namespace App\Http\Controllers;
283
+
284
+ use App\Models\Service;
285
+ use App\Models\Doctor;
286
+ use App\Models\Post;
287
+
288
+ class SitemapController extends Controller
289
+ {
290
+ public function __invoke()
291
+ {
292
+ $urls = collect()
293
+ ->push(['loc' => url('/'), 'lastmod' => now()->toDateString(), 'priority' => '1.0'])
294
+ ->merge(Service::published()->get()->map(fn ($s) => [
295
+ 'loc' => url("/uslugi/{$s->slug}/"),
296
+ 'lastmod' => $s->updated_at->toDateString(),
297
+ 'priority' => '0.8',
298
+ ]))
299
+ ->merge(Doctor::published()->get()->map(fn ($d) => [
300
+ 'loc' => url("/vrachi/{$d->slug}/"),
301
+ 'lastmod' => $d->updated_at->toDateString(),
302
+ 'priority' => '0.7',
303
+ ]))
304
+ ->merge(Post::published()->get()->map(fn ($p) => [
305
+ 'loc' => url("/blog/{$p->slug}/"),
306
+ 'lastmod' => $p->updated_at->toDateString(),
307
+ 'priority' => '0.6',
308
+ ]));
309
+
310
+ return response()
311
+ ->view('seo.sitemap', ['urls' => $urls])
312
+ ->header('Content-Type', 'application/xml; charset=UTF-8');
313
+ }
314
+ }
315
+ ```
316
+
317
+ ```blade
318
+ {{-- resources/views/seo/sitemap.blade.php --}}
319
+ {!! '<'.'?xml version="1.0" encoding="UTF-8"?'.'>' !!}
320
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
321
+ @foreach ($urls as $u)
322
+ <url>
323
+ <loc>{{ $u['loc'] }}</loc>
324
+ <lastmod>{{ $u['lastmod'] }}</lastmod>
325
+ <priority>{{ $u['priority'] }}</priority>
326
+ </url>
327
+ @endforeach
328
+ </urlset>
329
+ ```
330
+
331
+ > Осторожно с `{{ '<'.'?xml' }}`: Blade съедает `<?xml`. Альтернатива — `response()->view(...)->header(...)`
332
+ > с шаблоном, где первая строка выводится через `{!! !!}` с экранированием, или отдавать sitemap строкой
333
+ > из PHP (надёжнее). Кэшируй sitemap (`Cache::remember('sitemap', 3600, ...)`), но сбрасывай кэш при
334
+ > публикации/обновлении сущности.
335
+
336
+ ### 1.7 301-редиректы (middleware)
337
+
338
+ ```php
339
+ <?php
340
+ // app/Http/Middleware/CanonicalHost.php — http→https, www→без www, слэш
341
+ namespace App\Http\Middleware;
342
+
343
+ use Closure;
344
+ use Illuminate\Http\Request;
345
+
346
+ class CanonicalHost
347
+ {
348
+ public function handle(Request $request, Closure $next)
349
+ {
350
+ $host = $request->getHost();
351
+ $canonical = config('seo.canonical_host');
352
+ $needsRedirect = $host !== $canonical
353
+ || ! $request->isSecure()
354
+ || ($request->getPathInfo() !== '/' && str_ends_with($request->getPathInfo(), '/'));
355
+
356
+ if ($needsRedirect && ! app()->environment('local')) {
357
+ $path = rtrim($request->getPathInfo(), '/') ?: '/';
358
+ $query = $request->getQueryString();
359
+
360
+ return redirect()->to(
361
+ 'https://' . $canonical . $path . ($query ? '?' . $query : ''),
362
+ 301
363
+ );
364
+ }
365
+
366
+ return $next($request);
367
+ }
368
+ }
369
+ ```
370
+
371
+ Исторические редиректы (после смены URL) — таблицей в БД/конфиге + middleware `RedirectOldUrls`,
372
+ **один прыжок**, без цепочек. Для массовых правил — `nginx`/`.htaccess` (быстрее).
373
+
374
+ ### 1.8 Прочее по Laravel
375
+
376
+ - Роут-модель по `slug`, а не `id`; `Route::get('/uslugi/{service:slug}')`.
377
+ - 404: `abort(404)` — Laravel отдаст 404 и страницу `errors/404.blade.php` (должна содержать навигацию).
378
+ - `noindex` для страниц «спасибо»/личного кабинета: `Seo::page(...)->robots('noindex,follow')`.
379
+ - Пагинация: `?page=` + canonical на текущую страницу (`url()->full()` без лишних параметров).
380
+ - Кэш: `view:cache`, `route:cache`, `config:cache` на проде; после деплоя — `php artisan optimize` по политике проекта.
381
+ - Slug: `Str::slug($title)` транслитерирует кириллицу (`МРТ головы` → `mrt-golovy`). Проверяй уникальность и
382
+ не меняй slug у проиндексированных страниц без 301.
383
+ - Пакет `spatie/laravel-sitemap` — удобно, если не хочется свой контроллер; для маленького сайта проще свой.
384
+
385
+ ---
386
+
387
+ ## 2. 1С-Битрикс + Aspro «Медицинский центр 3.0»
388
+
389
+ ### 2.1 Где что лежит (и где нельзя править)
390
+
391
+ | Что | Где | Правило |
392
+ |---|---|---|
393
+ | Шаблон Aspro | `/local/templates/aspro-allcorp3medc/` | **правим только здесь**, не в `/bitrix/templates/` (маркетплейс) |
394
+ | `init.php` | `/local/php_interface/init.php` | свои события/редиректы/SEO-логика |
395
+ | `urlrewrite.php` | `/urlrewrite.php` + `/local/` | правила ЧПУ и 301 |
396
+ | `robots.txt`, `sitemap.xml` | генерируются модулем «Поисковая оптимизация» (seo) | настраивать в админке, не руками в корне |
397
+ | Ядро | `/bitrix/modules/` | **не трогать** |
398
+ | Кэш | `/bitrix/cache/`, `/bitrix/managed_cache/`, `/bitrix/stack_cache/` | чистить после правок |
399
+
400
+ Проверка окружения: `/bitrix/admin/` (админка), «Настройки → Настройки продукта → Настройки модулей →
401
+ Поисковая оптимизация» — SEO-шаблоны, robots, sitemap.
402
+
403
+ ### 2.2 SEO-шаблоны инфоблоков (главный инструмент)
404
+
405
+ В настройках инфоблока (вкладка **SEO**) задаются шаблоны для списка и детальной страницы:
406
+ `title`, `description`, `keywords`, `h1`, `canonical`, `robots`, `og:*`. Макросы (синтаксис Bitrix):
407
+
408
+ ```
409
+ {=this.name} — название текущего элемента/раздела
410
+ {=this.property.SEO_TITLE} — значение свойства
411
+ {=parent.name} — название родительского раздела
412
+ {=section.name} — раздел
413
+ {=iblock.name} — инфоблок
414
+ {=this.id} / {=this.code}
415
+ ```
416
+
417
+ Пример шаблона title для услуг: `{=this.name} в организация — цена, запись | {=parent.name}`
418
+ Пример description: `{=this.name}: показания, подготовка, стоимость и запись в {=parent.name}. Опытные врачи, современное оборудование.`
419
+
420
+ Правила:
421
+ - Не делай шаблон «{=this.name} — {=iblock.name}» для всего — дубли и отсутствие интента.
422
+ - Проверь каждую группу: шаблон применяется к **всем** элементам инфоблока, включая «пустые» свойства
423
+ (получишь «в организация — цена, запись» без названия).
424
+ - Уникальные title/description можно переопределять через свойства элемента (`SEO_TITLE`, `SEO_DESCRIPTION`)
425
+ и отдавать их в шаблоне компонента/`header.php`.
426
+ - После правки SEO-шаблона — **очистить кэш** (админка «Очистить кэш» или `/bitrix/admin/cache.php`)
427
+ и проверить живой HTML (`view-source`), иначе увидишь старый title.
428
+
429
+ ### 2.3 Мета в шаблоне (`header.php`) и компонентах
430
+
431
+ ```php
432
+ <?php
433
+ // /local/templates/aspro-allcorp3medc/header.php — фрагмент <head>
434
+ $APPLICATION->ShowHead();
435
+ ?>
436
+ <title><?php $APPLICATION->ShowTitle(); ?></title>
437
+ <?php
438
+ $canonical = $APPLICATION->GetPageProperty('canonical');
439
+ if (!$canonical) {
440
+ $canonical = 'https://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
441
+ }
442
+ $APPLICATION->AddHeadString('<link rel="canonical" href="' . htmlspecialcharsbx($canonical) . '">', true);
443
+ ?>
444
+ ```
445
+
446
+ Ключевые API:
447
+ - `$APPLICATION->SetTitle(...)` — `<h1>`/`<title>` (в Aspro часто используется для H1).
448
+ - `$APPLICATION->SetPageProperty('title', ...)`, `'description'`, `'keywords'`.
449
+ - `$APPLICATION->SetPageProperty('canonical', ...)` — если шаблон/компонент это поддерживает; иначе
450
+ `AddHeadString`.
451
+ - `$APPLICATION->AddHeadString('<meta ...>')` — для og/twitter (с `$unique=true`, чтобы не дублировать).
452
+ - Aspro-медицина: у компонентов (`bitrix:news.list`, `bitrix:news.detail`, `aspro:...`) есть параметры
453
+ `SET_TITLE`, `SET_STATUS_404`, `SHOW_404`, `MESSAGE_404` — **включай** `SET_STATUS_404=Y`,
454
+ иначе будет «мягкий 404» с кодом 200.
455
+
456
+ ### 2.4 Canonical, 404, `noindex` в Битриксе
457
+
458
+ - **Canonical** — на детальных страницах ставь на «чистый» URL без `?utm_*`, `?sort=`, `?PAGEN_*` (если это
459
+ фильтры), через `SetPageProperty('canonical', ...)`/`AddHeadString`.
460
+ - **Мягкие 404**: `$APPLICATION->SetStatus('404')` / `CHTTP::SetStatus('404 Not Found')` при пустом результате;
461
+ у компонентов — `SET_STATUS_404=Y`. Проверка: `curl -I https://site.ru/net-takoy-stranicy/` → 404.
462
+ - **noindex** для служебных страниц: `$APPLICATION->SetPageProperty('robots', 'noindex, follow')`
463
+ или `<meta name="robots">` в шаблоне; для целых разделов — «SEO → Настройки robots» / правила в админке.
464
+ - **ЧПУ**: «Настройки → Настройки продукта → Настройки модулей → Главный модуль → Обработка ЧПУ»;
465
+ правила — в `urlrewrite.php`; проверь, что `/uslugi/mrt/` отдаёт 200, а не редирект на `?SECTION_ID=`.
466
+ - **301**: правила в `urlrewrite.php` (массив `$arUrlRewrite`) или `init.php`:
467
+
468
+ ```php
469
+ <?php
470
+ // /local/php_interface/init.php — пример точечных 301
471
+ AddEventHandler('main', 'OnEpilog', function () {
472
+ $uri = strtok($_SERVER['REQUEST_URI'], '?');
473
+ $map = [
474
+ '/uslugi/mrt-golovy/' => '/uslugi/mrt/',
475
+ '/old-section/' => '/napravleniya/',
476
+ ];
477
+ if (isset($map[$uri]) && http_response_code() !== 301) {
478
+ LocalRedirect($map[$uri], false, '301 Moved Permanently');
479
+ }
480
+ });
481
+ ```
482
+
483
+ `LocalRedirect($url, $skip_security_check = false, $status = '302 Found')` — ставь 301 явно.
484
+
485
+ ### 2.5 robots.txt и sitemap.xml
486
+
487
+ - **robots.txt**: «Настройки → Настройки продукта → Настройки модулей → Поисковая оптимизация → robots.txt».
488
+ Автогенерация учитывает настройки, но проверяй результат на живом URL. Не закрывай `/bitrix/js`, `/bitrix/css`,
489
+ `/upload`; закрывай `/bitrix/` (кроме нужных подпапок), `/local/` (кроме шаблона), `/search/`, `/auth/`.
490
+ - **sitemap.xml**: тот же модуль → «Файлы Sitemap» / настройки генерации; проверь, что в карту попадают только
491
+ активные элементы с 200, что `<lastmod>` реальный (дата изменения элемента), и что карта доступна
492
+ `https://example.org/sitemap.xml`. Обновление — по агенту/по расписанию; после массовых правок — перегенерировать.
493
+ - **Яндекс**: отправить sitemap в Вебмастер; проверить «Анализ robots.txt».
494
+ - **Композитный сайт** (если включён): для авторизованных отдаётся динамика — проверяй SEO-мета в `view-source`
495
+ под гостем, иначе рискуешь увидеть «персонализированный» HTML.
496
+
497
+ ### 2.6 Aspro-медицина: частные грабли
498
+
499
+ - Шаблон медицины — много компонентов с `SET_TITLE`; если заголовок дублируется (`<h1>` из компонента + свой) —
500
+ ищи `SET_TITLE=Y` и отключай в одном месте.
501
+ - Услуги/врачи часто в разных инфоблоках и с разными URL — проверь, что карточка врача **одна**, а не
502
+ дублируется в каждом отделении (canonical или один источник).
503
+ - Формы записи/личный кабинет — `noindex`; страницы «спасибо» — `noindex`.
504
+ - Фильтры/сортировки (цены, алфавит) — не должны плодить индексируемые дубли: canonical на базовый URL
505
+ или `noindex,follow`; для Яндекса — `Clean-param`.
506
+ - Кэш: после правки шаблона/`init.php` чисти кэш сайта и **кэш компонентов**; для админа помогает `?clear_cache=Y`,
507
+ но не полагайся на него для проверки — используй `view-source` в приватном окне.
508
+ - Обновления Aspro: правки делать в `local/`, чтобы обновление шаблона их не снесло; `custom.css`/`init.php` —
509
+ штатные точки расширения (см. скилл `bitrix-aspro`).
510
+
511
+ ---
512
+
513
+ ## 3. React / Vite
514
+
515
+ ### 3.1 Главное решение: не SPA для контента
516
+
517
+ Для медицинского сайта/лендинга выбирай:
518
+ 1. **SSG** (Astro, Next.js `output: 'export'`, Eleventy) — лучший вариант: мета и JSON-LD в статике, CWV отличные.
519
+ 2. **SSR** (Next.js App Router, Remix, Laravel+Vite SSR) — если нужна динамика/личный кабинет.
520
+ 3. **SPA + prerender** — компромисс: на этапе сборки прогнать список маршрутов через headless-браузер и
521
+ сохранить HTML (`vite-plugin-ssr`/Vike, `puppeteer` prerender, `react-snap`). Требует поддержки списка URL.
522
+
523
+ **Не оставляй** контентные страницы на чистом CSR: title/meta/H1/ссылки должны быть в HTML.
524
+
525
+ ### 3.2 Astro (рекомендуемый вариант для статики)
526
+
527
+ ```astro
528
+ ---
529
+ // src/pages/uslugi/[slug].astro
530
+ import Layout from '../../layouts/Layout.astro';
531
+ const { service } = Astro.props;
532
+ ---
533
+ <Layout
534
+ title={`${service.title} в организация — цена от ${service.priceFrom} ₽`}
535
+ description={service.description.slice(0, 155)}
536
+ canonical={`https://example.org/uslugi/${service.slug}/`}
537
+ jsonLd={[{
538
+ '@context': 'https://schema.org',
539
+ '@type': 'MedicalWebPage',
540
+ url: `https://example.org/uslugi/${service.slug}/`,
541
+ name: service.title,
542
+ inLanguage: 'ru-RU',
543
+ lastReviewed: service.reviewedAt,
544
+ }]}
545
+ >
546
+ <h1>{service.title}</h1>
547
+ <!-- контент -->
548
+ </Layout>
549
+ ```
550
+
551
+ ```astro
552
+ ---
553
+ // src/layouts/Layout.astro
554
+ const { title, description, canonical, jsonLd = [] } = Astro.props;
555
+ ---
556
+ <!doctype html>
557
+ <html lang="ru">
558
+ <head>
559
+ <meta charset="utf-8" />
560
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
561
+ <title>{title}</title>
562
+ <meta name="description" content={description} />
563
+ <link rel="canonical" href={canonical} />
564
+ <meta property="og:type" content="website" />
565
+ <meta property="og:locale" content="ru_RU" />
566
+ <meta property="og:title" content={title} />
567
+ <meta property="og:description" content={description} />
568
+ <meta property="og:url" content={canonical} />
569
+ <meta property="og:image" content="https://example.org/img/og-default-1200x630.jpg" />
570
+ {jsonLd.map((node) => (
571
+ <script type="application/ld+json" set:html={JSON.stringify(node)} />
572
+ ))}
573
+ </head>
574
+ <body><slot /></body>
575
+ </html>
576
+ ```
577
+
578
+ ### 3.3 Next.js (App Router)
579
+
580
+ - `generateMetadata()` на каждую страницу → `title`, `description`, `alternates.canonical`, `openGraph`, `robots`.
581
+ - JSON-LD — обычный `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(node) }} />`.
582
+ - `app/not-found.tsx` + `notFound()` — реальный 404 (не отдавай 200 на неизвестный маршрут).
583
+ - `app/sitemap.ts` и `app/robots.ts` — генерируемые sitemap/robots.
584
+ - `next/image` для CWV (width/height, priority для LCP).
585
+ - `export const dynamic = 'force-static'` для контентных страниц.
586
+
587
+ ### 3.4 Клиентский React (если SPA неизбежна)
588
+
589
+ - React 19 умеет рендерить `<title>`/`<meta>`/`<link>` и «поднимать» их в `<head>`, но **только если есть SSR**.
590
+ В чистом CSR это не работает для роботов — не рассчитывай.
591
+ - `react-helmet-async` — только вместе с SSR/prerender.
592
+ - Роутинг: History API + сервер должен отдавать **реальный HTML** на каждый маршрут (SSR/SSG) и **404** на
593
+ неизвестные пути.
594
+ - CWV: `React.lazy`/динамические импорты, без тяжёлых библиотек в первом экране; шрифты — `font-display: swap`;
595
+ изображения — `width/height`, WebP/AVIF, lazy ниже фолда.
596
+ - Проверка: `view-source` + `curl -A Googlebot` — в ответе должны быть мета/контент/ссылки.
597
+
598
+ ---
599
+
600
+ ## 4. Статический HTML
601
+
602
+ ```html
603
+ <!doctype html>
604
+ <html lang="ru">
605
+ <head>
606
+ <meta charset="utf-8">
607
+ <meta name="viewport" content="width=device-width, initial-scale=1">
608
+ <title>Кардиология в организация, Москва — врачи, диагностика, запись</title>
609
+ <meta name="description" content="Кардиологическое отделение: ЭКГ, ЭхоКГ, холтер, консультации кардиолога. Запись онлайн и по телефону.">
610
+ <meta name="robots" content="index,follow">
611
+ <link rel="canonical" href="https://example.org/napravleniya/kardiologiya/">
612
+ <meta property="og:type" content="website">
613
+ <meta property="og:locale" content="ru_RU">
614
+ <meta property="og:title" content="Кардиология в организация, Москва — врачи, диагностика, запись">
615
+ <meta property="og:description" content="...">
616
+ <meta property="og:url" content="https://example.org/napravleniya/kardiologiya/">
617
+ <meta property="og:image" content="https://example.org/img/og-kardiologiya-1200x630.jpg">
618
+ <script type="application/ld+json">
619
+ {"@context":"https://schema.org","@type":"MedicalWebPage","url":"https://example.org/napravleniya/kardiologiya/","name":"Кардиология","inLanguage":"ru-RU","lastReviewed":"2026-10-20"}
620
+ </script>
621
+ </head>
622
+ <body>
623
+ <h1>Кардиология: врачи, диагностика и лечение</h1>
624
+ <!-- контент, ссылки, alt, width/height -->
625
+ </body>
626
+ </html>
627
+ ```
628
+
629
+ Для статики:
630
+ - **sitemap** генерируй скриптом из списка HTML-файлов (Node/PHP) — не пиши руками, забудешь страницу.
631
+ - **robots.txt** — файл в корне + `Sitemap: https://.../sitemap.xml`.
632
+ - Проверяй относительные пути: при открытии через `file://` ссылки работают, но в вебе нужны правильные корни.
633
+ - Хостинг: настроить 404-страницу, HTTPS, gzip/brotli, кеш статики.
634
+
635
+ ---
636
+
637
+ ## 5. Сервер/деплой: минимум для SEO
638
+
639
+ ### nginx
640
+
641
+ ```nginx
642
+ # http → https + www → без www + слэш
643
+ server {
644
+ listen 80;
645
+ server_name example.ru www.example.ru;
646
+ return 301 https://example.ru$request_uri;
647
+ }
648
+
649
+ server {
650
+ listen 443 ssl http2;
651
+ server_name www.example.ru;
652
+ return 301 https://example.ru$request_uri;
653
+ }
654
+
655
+ server {
656
+ listen 443 ssl http2;
657
+ server_name example.ru;
658
+ root /var/www/example/public;
659
+ # ... fastcgi/php
660
+ error_page 404 /404.html;
661
+ add_header Cache-Control "public, max-age=3600" always; # для статики — долгий immutable
662
+ }
663
+ ```
664
+
665
+ ### .htaccess (Apache/Битрикс)
666
+
667
+ ```apache
668
+ RewriteEngine On
669
+ RewriteCond %{HTTPS} !=on [OR]
670
+ RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
671
+ RewriteRule ^ https://example.ru%{REQUEST_URI} [R=301,L]
672
+ ```
673
+
674
+ ### Проверка после деплоя (5 минут)
675
+
676
+ ```bash
677
+ curl -sI https://example.ru/uslugi/mrt/ | head -20 # 200, content-type, кеш
678
+ curl -s https://example.ru/uslugi/mrt/ | grep -E 'title|canonical|og:'
679
+ curl -sI https://example.ru/net-takoy-stranicy/ | head -3 # 404
680
+ curl -s https://example.ru/robots.txt
681
+ curl -s https://example.ru/sitemap.xml | head -20
682
+ ```
683
+
684
+ Плюс: Rich Results Test для 2–3 типов страниц, `seo-audit.mjs` по странице/URL, GSC «Проверка URL».
685
+
686
+ ---
687
+
688
+ ## 6. Первоисточники
689
+
690
+ - Laravel: https://laravel.com/docs/blade · https://laravel.com/docs/routing
691
+ - Bitrix (техдокументация): https://dev.1c-bitrix.ru · модуль «Поисковая оптимизация» (seo)
692
+ - Aspro: документация шаблона «Медицинский центр 3.0» + скилл `bitrix-aspro` (локально)
693
+ - Astro: https://docs.astro.build · Next.js: https://nextjs.org/docs
694
+ - Google. Управление мета-тегами/JS: https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics
695
+ - Google. URL-структура: https://developers.google.com/search/docs/crawling-indexing/url-structure