Перейти к основному содержимому

SEO-оптимизация CareerClue — дизайн

Дата: 2026-07-18 Цель: рост органического трафика из Google и Yandex. Сайт: Docusaurus 3.10, блог из 293 постов (mock-interview разборы, Q&A формат), ru, https://careerclue.vercel.app.

Контекст и решения

Зафиксировано на этапе brainstorming:

  • Цель — комплексный рост органического трафика (техническое SEO + on-page).
  • Домен — остаёмся на careerclue.vercel.app, кастомный домен вне scope.
  • Backfill — полный, все 291 постов (без description) обрабатываются.
  • Title — НЕ трогаем (permalink и так из имени файла; риск/diff не оправдан).
  • OG-картинки per-post — отложены (Фаза 4, опционально).
  • FAQPage/QAPage schema — пропускаем (Google с 2023 отдаёт FAQ rich results только gov/health; ROI низкий).

Что уже работает (проверено в build/)

  • BlogPosting JSON-LD эмитится Docusaurus 3.x автоматически (headline, description, url, dates).
  • canonical, og:*, twitter:* присутствуют.
  • sitemap.xml генерируется (preset-classic), но без lastmod.
  • Yandex Metrica + yandex-verification + google-site-verification подключены.
  • IndexNow скрипт (scripts/indexnow-submit.js) есть.
  • Algolia поиск работает.

Дыры (что чиним)

  1. Meta description — только у 2 из 291 постов. У остальных Docusaurus авто-подставляет первый абзац, а он у всех начинается шаблоном «Сегодня мы разберём видео, в котором…» → почти-дубликаты meta description по всему сайту, не keyword-first, слабый CTR.
  2. Теги — 282 из 291 постов имеют tags: [go] независимо от темы (65 из 66 frontend/react-постов помечены go). Ломает топикальную кластеризацию и тег-хабы.
  3. robots.txt — отсутствует полностью.
  4. sitemap — без lastmod (нет сигнала свежести для Yandex/Google).
  5. BreadcrumbList — не эмитится для блога (только у docs). Упущенный breadcrumb rich result.

Вне scope (флаг)

Контент-mismatch: на части react/js-постов (~65) тела ответов местами написаны в контексте Go (артефакт генератора). Это не SEO-тюнинг, а регенерация контента — отдельный заход. В этом дизайне НЕ решается; теги/description будут выведены по реальной теме из title+тела, но сами тела не переписываются.

Приоритизированный план

Подход: технически-первый — быстрые глобальные code-правки применяются ко всем 293 постам сразу, затем LLM-backfill контента.

Фаза 0 — База (код)

0.1 static/robots.txt

User-agent: *
Allow: /

Sitemap: https://careerclue.vercel.app/sitemap.xml

Явная Sitemap: директива — основной способ, которым Yandex находит sitemap. Кладётся в static/, копируется в build как есть.

0.2 Sitemap lastmod В docusaurus.config.ts заменить дефолтный sitemap preset-classic на явную конфигурацию:

sitemap: {
lastmod: 'date',
changefreq: 'weekly',
priority: 0.5,
},

Опция lastmod: 'date' (поддержана @docusaurus/plugin-sitemap 3.6+) добавляет <lastmod> из даты поста.

Verify: npm run buildbuild/robots.txt существует и содержит Sitemap; build/sitemap.xml содержит <lastmod> в url-записях постов.

Фаза 1 — BreadcrumbList JSON-LD

Swizzle-wrap theme/BlogPostPage (--wrap, не eject). В обёртке через useBlogPost() получить metadata поста и вставить в <Head> второй <script type="application/ld+json"> со схемой:

{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Главная", "item": "https://careerclue.vercel.app/"},
{"@type": "ListItem", "position": 2, "name": "Разборы", "item": "https://careerclue.vercel.app/blog"},
{"@type": "ListItem", "position": 3, "name": "<title поста>", "item": "<permalink поста>"}
]
}

BlogPosting уже эмитится — при swizzle проверить его полноту (author, image, datePublished); допилить только если поля пустые.

Verify: npm run build → пост содержит два JSON-LD блока; BreadcrumbList проходит валидатор schema.org (структурная проверка вручную/скриптом).

Фаза 2 — Контент-backfill (LLM)

2.1 Скрипт scripts/seo-backfill.js (Node, стиль как build-blog-index.js)

Для каждого blog/*.md*:

  • Парсит frontmatter + тело.
  • Idempotent: пропускает пост, если description уже есть (флаг --force для перегенерации).
  • Формирует промпт: title + первые ~2000 символов тела → просит вернуть строгий JSON:
    {"description": "...", "tags": ["...", "..."]}
    • description: 150–160 символов, keyword-first, уникальный, ru, без обрыва.
    • tags: тема из фиксированного словаря {go, frontend, react, javascript, typescript, system_design, backend} + грейд {junior, middle, senior} если явно определим. Тема выводится по РЕАЛЬНОМУ содержанию title+тела, а не по старому go.
  • Вызывает OpenRouter free-модель (OPENROUTER_API_KEY из env, тот же ключ что у Go-генератора; endpoint https://openrouter.ai/api/v1/chat/completions). Retry + вращение free-моделей при ошибке/rate-limit.
  • Пишет description и tags обратно во frontmatter, сохраняя остальные поля и тело без изменений.

Батч-стратегия: обрабатывать пачками (напр. по 25), git add -A && git commit после каждой успешной пачки — чекпоинт для отката. Лог прогресса + список fail-ов в конце.

2.2 Go-генератор (будущие посты) В mock/chatgtp (usecase.go / промпт генерации) добавить в выдаваемый frontmatter description и реальный tags — чтобы новые посты из пайплайна не регрессировали к пустому description и [go]. Минимальная правка промпта + шаблона записи.

Verify: после прогона grep -c '^description:' blog/*.md* ≈ 291; распределение tags больше не 282×[go]; npm run build без ошибок frontmatter; выборочно 5 постов — description осмысленный и релевантный теме.

Фаза 3 — Тег-хабы + перелинковка

С реальными тегами Docusaurus авто-создаёт /blog/tags/<tag> — топикальные лендинги.

  • Проверить, что тег-страницы попадают в sitemap.xml.
  • Убедиться, что теги кликабельны в посте (стандартный blog theme — да).
  • Опционально: короткие описания тегов через blog/tags.yml (permalink + description на хаб).

Verify: build/blog/tags/<tag>/index.html существуют для каждого нового тега и присутствуют в sitemap.

Фаза 4 — OG-картинки per-post (ОТЛОЖЕНО)

Авто-генерация OG-картинок (title на брендовом шаблоне) на build. Не реализуется в этом заходе; вынесено как отдельная задача.

Off-page (действия пользователя, документируются)

  • Подать sitemap.xml в Google Search Console и Yandex Webmaster.
  • После деплоя дернуть IndexNow (npm run indexnow) для новых/изменённых URL.
  • Мониторить индексацию и Core Web Vitals.

Я подготовлю чеклист этих шагов; выполняет пользователь (доступ к панелям).

Порядок выполнения и критерии успеха

ФазаАртефактКритерий готовности
0robots.txt, sitemap configbuild содержит robots.txt + <lastmod> в sitemap
1swizzled BlogPostPage2 JSON-LD блока в посте, BreadcrumbList валиден
2seo-backfill.js + Go-правка~291 постов с description, теги по темам, build зелёный
3тег-хабытег-страницы в sitemap
4отложено

Риски

  • LLM-качество description/тегов — free-модели могут врать/выдавать невалидный JSON. Митигация: строгий JSON-парсинг + retry + вращение моделей + git-чекпоинты по батчам + выборочная ручная проверка.
  • Массовый diff (291 файл) — только frontmatter меняется; тела не трогаются. Батч-коммиты дают гранулярный откат.
  • Swizzle при обновлении Docusaurus--wrap (не eject) минимизирует поломки при апгрейдах.