React SEO: 4-строчный фикс для клиентского рендеринга

#react#seo#meta-tags

Недавно разбирался с кейсом, когда в production-окружении Google показывал страницы React-приложения как “Untitled Page”, хотя react-helmet был подключен и вроде работал. Оказалось, проблема не в отсутствии тегов, а в их позднем появлении — crawler’ы просто не дожидались выполнения JS. Расскажу про рабочий фикс без перехода на SSR.

Проблема: мета-теги вне рендер-цикла

В классическом клиентском React мета-теги обновляются асинхронно, часто через useEffect или хуки роутера. Но crawler’ы работают в жестких временных рамках:

// Типичный пример с react-helmet (неоптимально)
function Page() {
  return (
    <>
      <Helmet>
        <title>My Page</title> {/* Сработает слишком поздно */}
      </Helmet>
      <Content />
    </>
  );
}

Решение — синхронное обновление тегов через document API:

// SEO синхронизированный с рендером
function useSyncSEO({ title, description }) {
  // Без useEffect - выполняем сразу
  if (typeof document !== 'undefined') {
    document.title = title;
    updateMetaTag('description', description);
    updateMetaTag('og:title', title, true);
  }
}

function updateMetaTag(name, content, isProperty = false) {
  const attr = isProperty ? 'property' : 'name';
  let tag = document.querySelector(`meta[${attr}="${name}"]`);
  
  if (!tag) {
    tag = document.createElement('meta');
    tag.setAttribute(attr, name);
    document.head.appendChild(tag);
  }
  tag.setAttribute('content', content);
}

Кейсы, где это критично:

Когда pre-render лучше SSR

Полноценный SSR с Next.js — идеальное решение, но не всегда feasible. Для лендингов и статических страниц работает pre-rendering через Puppeteer:

// prerender.js
const routes = ['/', '/about'];
const browser = await puppeteer.launch();

for (const route of routes) {
  const page = await browser.newPage();
  await page.goto(`http://localhost:3000${route}`);
  const html = await page.content();
  fs.writeFileSync(`build${route}/index.html`, html);
}

Лайфхак: можно интегрировать этот шаг в CI/CD pipeline перед деплоем.

Sitemap и JSON-LD — обязательный минимум

Даже с синхронными мета-тегами crawler’ам нужны подсказки:

  1. Генерация sitemap.xml из роутов:
const dynamicRoutes = await fetchProductSlugs(); // [/products/1, ...]
const sitemap = `<urlset>
  ${dynamicRoutes.map(url => `<url><loc>${baseUrl}${url}</loc></url>`)}
</urlset>`;
  1. JSON-LD для rich snippets:
<script type="application/ld+json">
  {JSON.stringify({
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": "React SEO Fixes"
  })}
</script>

Когда ручное управление становится болью

На проекте с 50+ роутами мы столкнулись с проблемой:

Перешли на конфигурационный подход:

// seo.config.js
export default {
  '/products/:id': {
    title: ({ product }) => `${product.name} | Store`,
    schema: ({ product }) => ({ '@type': 'Product' })
  }
}

// SEOProvider.jsx
function useRouteSEO() {
  const { pathname } = useLocation();
  const routeConfig = matchPath(pathname, seoConfig);
  useSyncSEO(routeConfig);
}

Выводы

  1. Timing matters — crawler’ы не ждут useEffect
  2. Pre-render дешевле SSR для статических страниц
  3. Sitemap + JSON-LD дают immediate boost
  4. Конфиги > ручное управление на scale

Для глубокого погружения рекомендую проверить:

P.S. Если у вас есть war stories про SEO в SPA — делитесь в комментах, соберем анти-паттерны.


Источник: https://dev.to/mitudas/how-to-fix-seo-issues-in-react-apps-the-4-line-fix-that-actually-worked-59a4