Недавно разбирался с кейсом, когда в 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);
}
Кейсы, где это критично:
- Twitter/Slack previews
- SEO-аудиты типа Ahrefs
- Ускоренные мобильные страницы (AMP)
Когда 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’ам нужны подсказки:
- Генерация sitemap.xml из роутов:
const dynamicRoutes = await fetchProductSlugs(); // [/products/1, ...]
const sitemap = `<urlset>
${dynamicRoutes.map(url => `<url><loc>${baseUrl}${url}</loc></url>`)}
</urlset>`;
- JSON-LD для rich snippets:
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "React SEO Fixes"
})}
</script>
Когда ручное управление становится болью
На проекте с 50+ роутами мы столкнулись с проблемой:
- Разные форматы meta-данных между командами
- Потерянные теги на вложенных страницах
- Дублирование кода для OpenGraph
Перешли на конфигурационный подход:
// 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);
}
Выводы
- Timing matters — crawler’ы не ждут useEffect
- Pre-render дешевле SSR для статических страниц
- Sitemap + JSON-LD дают immediate boost
- Конфиги > ручное управление на 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