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.
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/client.js +4101 -0
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/COMPATIBILITY.md +61 -0
- package/docs/INSTALL.md +84 -0
- package/docs/PRIVACY.md +50 -0
- package/docs/ROADMAP.md +57 -0
- package/docs/TOKENS.md +118 -0
- package/docs/TROUBLESHOOTING.md +68 -0
- package/docs/images/hero.png +0 -0
- package/index.js +84 -0
- package/package.json +70 -4
- package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
- package/plugins/dsh-gkb-ru-ui/lib/client.js +1331 -0
- package/plugins/dsh-gkb-ru-ui/lib/index.js +186 -0
- package/plugins/dsh-gkb-ru-ui/lib/skills.js +570 -0
- package/plugins/dsh-gkb-ru-ui/package.json +28 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/README.md +70 -0
- package/plugins/dsh-peak-guard/client.js +741 -0
- package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
- package/plugins/dsh-peak-guard/icon.svg +12 -0
- package/plugins/dsh-peak-guard/index.js +2 -0
- package/plugins/dsh-peak-guard/locale/en.json +6 -0
- package/plugins/dsh-peak-guard/locale/ru.json +6 -0
- package/plugins/dsh-peak-guard/locale/zh.json +6 -0
- package/plugins/dsh-peak-guard/package.json +33 -0
- package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
- package/plugins/dsh-profile-transfer/README.md +62 -0
- package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
- package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
- 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
- package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
- 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
- package/plugins/dsh-profile-transfer/client.js +806 -0
- package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
- package/plugins/dsh-profile-transfer/icon.svg +13 -0
- package/plugins/dsh-profile-transfer/index.js +375 -0
- package/plugins/dsh-profile-transfer/locale/en.json +6 -0
- package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
- package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
- package/plugins/dsh-profile-transfer/package.json +36 -0
- package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
- package/plugins/dsh-profile-transfer/transfer.js +1004 -0
- package/plugins/dsh-project-memory/README.md +71 -0
- package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
- package/plugins/dsh-project-memory/icon.svg +12 -0
- package/plugins/dsh-project-memory/index.js +1240 -0
- package/plugins/dsh-project-memory/locale/en.json +6 -0
- package/plugins/dsh-project-memory/locale/ru.json +6 -0
- package/plugins/dsh-project-memory/locale/zh.json +6 -0
- package/plugins/dsh-project-memory/package.json +26 -0
- package/plugins/dsh-session-move/README.md +67 -0
- package/plugins/dsh-session-move/client.js +719 -0
- package/plugins/dsh-session-move/cordis.patch.yml +5 -0
- package/plugins/dsh-session-move/icon.svg +14 -0
- package/plugins/dsh-session-move/index.js +497 -0
- package/plugins/dsh-session-move/locale/en.json +6 -0
- package/plugins/dsh-session-move/locale/ru.json +6 -0
- package/plugins/dsh-session-move/locale/zh.json +6 -0
- package/plugins/dsh-session-move/package.json +31 -0
- package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
- package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
- package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
- package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
- package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
- package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
- package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
- package/plugins/dsh-skill-autopilot/index.js +444 -0
- package/plugins/dsh-skill-autopilot/package.json +19 -0
- package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
- package/plugins/dsh-token-lab/lib/client.js +504 -0
- package/plugins/dsh-token-lab/lib/index.js +275 -0
- package/plugins/dsh-token-lab/package.json +26 -0
- package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
- package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
- package/skills/code-review-checklist/SKILL.md +55 -0
- package/skills/content-writing/README.md +87 -0
- package/skills/content-writing/SKILL.md +85 -0
- package/skills/content-writing/assets/checklists/fact-check.md +53 -0
- package/skills/content-writing/assets/checklists/publish.md +65 -0
- package/skills/content-writing/assets/checklists/social.md +40 -0
- package/skills/content-writing/assets/templates/article.md +47 -0
- package/skills/content-writing/assets/templates/brief.md +44 -0
- package/skills/content-writing/assets/templates/doctor-card.md +41 -0
- package/skills/content-writing/assets/templates/image-prompts.md +128 -0
- package/skills/content-writing/assets/templates/post-telegram.md +36 -0
- package/skills/content-writing/assets/templates/post-vk.md +42 -0
- package/skills/content-writing/assets/templates/press-release.md +56 -0
- package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
- package/skills/content-writing/assets/templates/service-card.md +46 -0
- package/skills/content-writing/references/anti-ai-style.md +248 -0
- package/skills/content-writing/references/editing.md +185 -0
- package/skills/content-writing/references/fact-checking.md +205 -0
- package/skills/content-writing/references/government-medical.md +246 -0
- package/skills/content-writing/references/images.md +212 -0
- package/skills/content-writing/references/journalism.md +252 -0
- package/skills/content-writing/references/process.md +123 -0
- package/skills/content-writing/references/quick-recipes.md +99 -0
- package/skills/content-writing/references/rules.md +85 -0
- package/skills/content-writing/references/science-popular.md +218 -0
- package/skills/content-writing/references/seo-content.md +172 -0
- package/skills/content-writing/references/social-media.md +253 -0
- package/skills/content-writing/references/sources.md +49 -0
- package/skills/content-writing/references/templates.md +291 -0
- package/skills/context-economy/SKILL.md +60 -0
- package/skills/context-economy/references/long-session.md +35 -0
- package/skills/context-economy/references/token-math.md +42 -0
- package/skills/context-economy/references/tool-recipes.md +34 -0
- package/skills/database-safety/SKILL.md +47 -0
- package/skills/debugging-playbook/SKILL.md +42 -0
- package/skills/design-critique/SKILL.md +65 -0
- package/skills/design-critique/references/a11y.md +25 -0
- package/skills/design-critique/references/diagnoses.md +33 -0
- package/skills/design-critique/references/quick-tests.md +30 -0
- package/skills/design-critique/references/report-template.md +63 -0
- package/skills/design-critique/references/rubric-fast.md +73 -0
- package/skills/dev-workflow/SKILL.md +48 -0
- package/skills/dev-workflow/references/definition-of-done.md +44 -0
- package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
- package/skills/dev-workflow/references/stages.md +67 -0
- package/skills/git-and-ci/SKILL.md +60 -0
- package/skills/pg-upgrade-verify/SKILL.md +67 -0
- package/skills/project-deep-dive/SKILL.md +45 -0
- package/skills/project-deep-dive/references/recon-commands.md +90 -0
- package/skills/project-deep-dive/references/section-templates.md +104 -0
- package/skills/project-deep-dive/references/tool-and-process.md +86 -0
- package/skills/project-memory-maintenance/SKILL.md +55 -0
- package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
- package/skills/project-memory-maintenance/references/journal-template.md +50 -0
- package/skills/project-memory-maintenance/references/section-rules.md +59 -0
- package/skills/release-and-deploy/SKILL.md +53 -0
- package/skills/requirements-and-planning/SKILL.md +49 -0
- package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
- package/skills/requirements-and-planning/references/process-steps.md +52 -0
- package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
- package/skills/security-review/SKILL.md +40 -0
- package/skills/seo/README.md +60 -0
- package/skills/seo/SKILL.md +77 -0
- package/skills/seo/assets/README.md +66 -0
- package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/seo/assets/scripts/README.md +179 -0
- package/skills/seo/assets/scripts/lib/args.mjs +105 -0
- package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
- package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
- package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
- package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
- package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
- package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
- package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
- package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
- package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
- package/skills/seo/assets/snippets/meta.blade.php +61 -0
- package/skills/seo/assets/snippets/robots.txt +61 -0
- package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
- package/skills/seo/assets/snippets/sitemap.xml +36 -0
- package/skills/seo/references/antipatterns.md +66 -0
- package/skills/seo/references/audit-checklist.md +319 -0
- package/skills/seo/references/content-seo.md +204 -0
- package/skills/seo/references/eeat-medical.md +219 -0
- package/skills/seo/references/measurement.md +250 -0
- package/skills/seo/references/on-page.md +214 -0
- package/skills/seo/references/page-templates.md +56 -0
- package/skills/seo/references/stack-recipes.md +695 -0
- package/skills/seo/references/structured-data.md +487 -0
- package/skills/seo/references/technical-seo.md +296 -0
- package/skills/service-desk-conventions/SKILL.md +52 -0
- package/skills/service-desk-db-safety/SKILL.md +63 -0
- package/skills/service-desk-release/SKILL.md +61 -0
- package/skills/service-desk-testing/SKILL.md +61 -0
- package/skills/service-desk-ui/SKILL.md +52 -0
- package/skills/skills-router/SKILL.md +53 -0
- package/skills/skills-router/references/applying-skills.md +25 -0
- package/skills/skills-router/references/routing-matrix.md +52 -0
- package/skills/testing-strategy/SKILL.md +52 -0
- package/skills/ui-design-system/SKILL.md +50 -0
- package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
- package/skills/ui-design-system/references/tokens-and-components.md +67 -0
- package/skills/ui-design-system/references/visual-qa.md +44 -0
- package/skills/web-research/SKILL.md +47 -0
- package/skills/web-research/references/answer-and-citations.md +60 -0
- package/skills/web-research/references/query-recipes.md +44 -0
- package/skills/wow-design/SKILL.md +89 -0
- package/skills/wow-design/assets/kit/README.md +195 -0
- package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
- package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
- package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
- package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
- package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
- package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
- package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
- package/skills/wow-design/assets/kit/showcase.html +1164 -0
- package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
- package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/wow-design/assets/scripts/README.md +312 -0
- package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
- package/skills/wow-design/assets/scripts/capture.mjs +337 -0
- package/skills/wow-design/assets/scripts/package.json +30 -0
- package/skills/wow-design/references/anti-patterns.md +252 -0
- package/skills/wow-design/references/critique-rubric.md +262 -0
- package/skills/wow-design/references/design-brief-template.md +48 -0
- package/skills/wow-design/references/inspiration.md +203 -0
- package/skills/wow-design/references/presets.md +99 -0
- package/skills/wow-design/references/principles.md +371 -0
- package/skills/wow-design/references/quick-presets.md +50 -0
- package/skills/wow-design/references/report-template.md +21 -0
- package/skills/wow-design/references/stack-recipes.md +96 -0
- package/skills/wow-design/references/visual-qa.md +82 -0
- package/skills/wow-design/references/wow-levers.md +43 -0
- 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
|