← Назад к вопросам

Как решишь задачу отдавать SEO-ботам сгенерированную статику а пользователям всю страницу?

3.0 Senior🔥 31 комментариев
#Архитектура и паттерны#Оптимизация и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

SEO: динамический контент для ботов vs полная страница для пользователей

Это критическая задача для современных веб-приложений: отдавать оптимизированный статический контент поисковым ботам и полнофункциональный интерактивный контент пользователям.

1. Server-Side Rendering (SSR) с Next.js

Самый современный и рекомендуемый подход.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Output для standalone сборки на Dokku
  output: 'standalone',
  // Включаем SSR/ISR
  experimental: {
    isrMemoryCacheSize: 50 * 1024 * 1024, // 50MB
  },
};

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Мой сайт',
  description: 'Описание для SEO',
  openGraph: {
    title: 'Мой сайт',
    description: 'Описание для SEO',
    url: 'https://example.com',
    type: 'website',
    images: [
      {
        url: 'https://example.com/og-image.jpg',
        width: 1200,
        height: 630,
      },
    ],
  },
};

// app/page.tsx
import { Suspense } from 'react';
import { InteractiveContent } from '@/components/interactive';

export default function Home() {
  return (
    <main>
      <h1>Заголовок для SEO</h1>
      <p>Статический контент для поисковых ботов</p>
      
      {/* Интерактивный контент загружается после гидрации */}
      <Suspense fallback={<div>Loading...</div>}>
        <InteractiveContent />
      </Suspense>
    </main>
  );
}

2. Обнаружение User-Agent

Определяем, бот это или реальный пользователь, и отдаём разный контент.

// lib/bot-detector.ts
export function isBot(userAgent: string): boolean {
  const botPatterns = [
    /googlebot/i,
    /bingbot/i,
    /slurp/i,
    /duckduckgo/i,
    /baiduspider/i,
    /yandexbot/i,
    /facebookexternalhit/i,
    /twitterbot/i,
    /linkedinbot/i,
    /whatsapp/i,
    /slack/i,
    /telegram/i,
  ];

  return botPatterns.some(pattern => pattern.test(userAgent));
}

// middleware.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server';
import { isBot } from '@/lib/bot-detector';

export function middleware(request: NextRequest) {
  const userAgent = request.headers.get('user-agent') || '';
  const botDetected = isBot(userAgent);
  
  // Передаём информацию о боте в заголовке
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-is-bot', botDetected.toString());
  
  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

3. Dynamic Content Rendering

Отдаём разный контент в зависимости от UA.

// components/content-dispatcher.tsx
import { headers } from 'next/headers';

export async function ContentDispatcher() {
  const headersList = await headers();
  const isBot = headersList.get('x-is-bot') === 'true';

  if (isBot) {
    return <SEOOptimizedContent />;
  }

  return <FullInteractiveContent />;
}

// Для SEO ботов
function SEOOptimizedContent() {
  return (
    <article>
      <h1>Полный заголовок статьи</h1>
      <p>Дружественный для SEO контент с ключевыми словами</p>
      
      <section>
        <h2>Раздел 1</h2>
        <p>Подробное описание...</p>
      </section>
      
      <section>
        <h2>Раздел 2</h2>
        <p>Дополнительная информация...</p>
      </section>
      
      <nav className="breadcrumb">
        <a href="/">Home</a>
        <a href="/articles">Articles</a>
        <span>Current Article</span>
      </nav>
    </article>
  );
}

// Для реальных пользователей
function FullInteractiveContent() {
  return (
    <article>
      <h1>Полный заголовок статьи</h1>
      
      <aside>
        <TableOfContents />
      </aside>
      
      <section>
        <h2>Раздел 1</h2>
        <InteractiveSection />
      </section>
      
      <section>
        <h2>Раздел 2</h2>
        <Comments />
      </section>
      
      <aside>
        <RelatedArticles />
        <Newsletter />
      </aside>
    </article>
  );
}

4. Static Generation с ISR

Генерируем статику на запрос и кэшируем.

// app/articles/[slug]/page.tsx
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  // Генерируем популярные статьи при сборке
  const articles = await fetch('https://api.example.com/articles/popular')
    .then(r => r.json());

  return articles.map((article: any) => ({
    slug: article.slug,
  }));
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const article = await getArticle(params.slug);

  return {
    title: article.title,
    description: article.description,
    openGraph: {
      title: article.title,
      description: article.description,
      type: 'article',
      url: `https://example.com/articles/${article.slug}`,
      images: [{ url: article.image }],
      publishedTime: article.published,
      authors: [article.author],
    },
  };
}

// ISR - Incremental Static Regeneration
export const revalidate = 3600; // Переге регулярно каждый час

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const article = await getArticle(params.slug);

  if (!article) notFound();

  return (
    <article>
      <h1>{article.title}</h1>
      <time>{article.published}</time>
      <p className="lead">{article.description}</p>
      
      {/* Статический контент для SEO */}
      <div dangerouslySetInnerHTML={{ __html: article.html }} />
      
      {/* Динамический контент */}
      <aside>
        <RelatedArticles slug={params.slug} />
        <Comments slug={params.slug} />
      </aside>
    </article>
  );
}

async function getArticle(slug: string) {
  const res = await fetch(`https://api.example.com/articles/${slug}`, {
    next: { revalidate: 3600 },
  });

  if (!res.ok) return null;
  return res.json();
}

5. JSON-LD для структурированных данных

Помогаем ботам понимать контент структурированные данные.

// components/structured-data.tsx
export function ArticleStructuredData({
  title,
  description,
  image,
  author,
  published,
  modified,
  content,
}: ArticleProps) {
  const jsonld = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: title,
    description,
    image,
    author: {
      '@type': 'Person',
      name: author,
    },
    datePublished: published,
    dateModified: modified,
    articleBody: content,
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonld) }}
    />
  );
}

// Использование
export default function Article() {
  return (
    <>
      <ArticleStructuredData
        title="Моя статья"
        description="Описание"
        image="https://example.com/image.jpg"
        author="Иван Петров"
        published="2024-01-01"
        modified="2024-01-15"
        content="Содержание статьи..."
      />
      
      <article>
        {/* ... контент ... */}
      </article>
    </>
  );
}

6. Robots.txt и Sitemap

Управляем доступом ботов.

// public/robots.txt
User-agent: *
Allow: /
Disallow: /admin
Disallow: /private
Disallow: /*.json$

Sitemap: https://example.com/sitemap.xml

User-agent: AdsBot-Google
Allow: /
// app/sitemap.ts (Next.js 13+)
import { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const articles = await fetch('https://api.example.com/articles')
    .then(r => r.json());

  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    ...articles.map((article: any) => ({
      url: `https://example.com/articles/${article.slug}`,
      lastModified: new Date(article.modified),
      changeFrequency: 'weekly',
      priority: 0.8,
    })),
  ];
}

7. Полная реализация с кэшированием

// app/page.tsx
import { cache } from 'react';
import { isBot } from '@/lib/bot-detector';

// Кэшируем результат fetch на время рендера
const getCachedData = cache(async () => {
  return fetch('https://api.example.com/data', {
    next: { revalidate: 300 }, // 5 минут
  }).then(r => r.json());
});

export const revalidate = 300;

export async function generateMetadata() {
  const data = await getCachedData();
  
  return {
    title: data.title,
    description: data.description,
    openGraph: {
      images: [{ url: data.image }],
    },
  };
}

export default async function Home() {
  const data = await getCachedData();
  const userAgent = (await import('next/headers')).headers().get('user-agent') || '';
  const bot = isBot(userAgent);

  return (
    <main>
      {/* Всегда показываем SEO-оптимальный контент */}
      <h1>{data.title}</h1>
      <p>{data.description}</p>
      <img src={data.image} alt={data.title} />
      
      {/* Интерактивный контент только для реальных пользователей */}
      {!bot && (
        <>
          <Comments articleId={data.id} />
          <InteractiveFeatures />
          <Newsletter />
        </>
      )}
    </main>
  );
}

8. Prerendering для критических страниц

// scripts/prerender.js
const fs = require('fs');
const path = require('path');

async function prerender() {
  const routes = [
    '/',
    '/about',
    '/contact',
    '/articles',
  ];

  for (const route of routes) {
    const html = await renderToString(route);
    const filepath = path.join(process.cwd(), '.next', 'prerendered', route + '.html');
    
    fs.mkdirSync(path.dirname(filepath), { recursive: true });
    fs.writeFileSync(filepath, html);
    
    console.log(`Prerendered: ${route}`);
  }
}

prerender();

Best Practices для SEO

  1. Используй Next.js с SSR/ISR - лучший выбор для SEO
  2. Генерируй metadata динамически - актуальные OpenGraph теги
  3. Добавляй JSON-LD - структурированные данные для ботов
  4. Оптимизируй images - используй next/image с alt текстами
  5. Кэшируй агрессивно - ISR для часто запрашиваемых страниц
  6. Проверяй мобильность - Page Speed Insights очень важен для ранкинга
  7. Тестируй с Google Search Console - видишь проблемы рано
  8. Используй robots.txt и sitemap - помогает ботам ориентироваться
  9. Избегай JavaScript-only контента - боты не всегда выполняют JS
  10. Мониторь Core Web Vitals - LCP, FID, CLS критичны для SEO

Лучшая стратегия: SSR/ISR с Next.js + User-Agent detection + JSON-LD. Это гарантирует что боты получат оптимизированный контент, а пользователи полнофункциональное приложение.

Как решишь задачу отдавать SEO-ботам сгенерированную статику а пользователям всю страницу? | PrepBro