VibeCoderzVibeCoderz
Все статьи
Бизнес и заработок2026/08/047 мин чтения

SEO портал на Next.js: структура маршрутов данные sitemap и schema

Содержание (8)+

SEO-архитектура портала на Next.js строится на трех вещах: продуманных маршрутах App Router, файлах sitemap.ts и robots.ts, и JSON-LD разметке schema.org для каждого типа страницы. Без этой связки Google и Яндекс просто не находят половину каталога, а нейровыдача вроде Google AI Overview вообще не подхватывает страницу. Ниже разберем архитектуру на примере каталога и блога: как устроены маршруты, откуда брать данные для sitemap и какую schema ставить под инструмент, статью и FAQ.

В статье: структура маршрутов для каталога и блога, готовый код динамического sitemap с sitemap index для больших каталогов, robots.ts с закрытыми путями и три JSON-LD схемы под разные типы контента. Все примеры на Next.js 14 App Router, проверены на проекте с 6 200 материалами.

Изображение

Как устроена маршрутизация SEO-каталога в Next.js App Router

Каждый тип контента получает свой сегмент: /blog/[slug] для статей, /item/[slug] для инструментов, /category/[slug] для категорий. Разделение по сегментам дает независимую генерацию, кеширование и метаданные для каждого типа.

На портале с каталогом и блогом обычно четыре базовых маршрута: app/blog/[slug]/page.tsx для статей, app/item/[slug]/page.tsx для карточек инструментов, app/category/[slug]/page.tsx для листингов и app/agents/page.tsx для отдельного каталога. У каждого сегмента своя функция generateStaticParams, которая на этапе сборки заранее подтягивает список slug из базы или CMS.

Вот в чем разница с Pages Router. Там маршруты собирались вручную через getStaticPaths, и SEO-логика размазывалась по _document.tsx и next/head. В App Router метаданные живут прямо в файле страницы через экспорт generateMetadata, а статика генерируется автоматически при билде.

// app/item/[slug]/page.tsx
export async function generateStaticParams() {
  const tools = await getAllTools(); // slug, updatedAt
  return tools.map((tool) => ({ slug: tool.slug }));
}

export const revalidate = 86400; // ISR раз в сутки

export async function generateMetadata({ params }) {
  const tool = await getToolBySlug(params.slug);
  return {
    title: `${tool.name}: обзор, цены и альтернативы 2026`,
    description: tool.shortDescription,
    alternates: { canonical: `https://vibecoderz.ru/item/${tool.slug}` },
  };
}

Для каталога с сотнями инструментов и тысячами статей полная статика на билде становится долгой. Здесь помогает ISR: revalidate = 86400 держит страницу свежей раз в сутки без пересборки всего проекта. Для новостей и часто обновляемых карточек ставят revalidate = 3600, раз в час.

Изображение

Как generateStaticParams ускоряет индексацию каталога

generateStaticParams заранее генерирует список маршрутов на этапе сборки, поэтому краулер получает готовый HTML вместо ожидания рендера на лету. Это сокращает время до первого байта и повышает шанс попасть в индекс с первого захода бота.

Роботы Google и Яндекса не любят ждать. Если страница рендерится динамически при каждом запросе, краулер получает медленный ответ и может пометить URL как низкоприоритетный. generateStaticParams решает это на корню: все известные slug превращаются в статические HTML-файлы еще на этапе next build.

Для портала с каталогом инструментов и блогом это выглядит так: функция обходит все таблицы item и blogPost, забирает slug и updatedAt, и передает их в билд. Новые страницы, которых не было на момент сборки, обрабатываются через dynamicParams: true по умолчанию, то есть Next.js сам дорендерит их при первом заходе и закеширует.

Практический нюанс: если в каталоге 5000+ карточек, полный билд может занять 10-15 минут. В таком случае часть каталога стоит оставить на чистый ISR без предгенерации, а в статику включать только топ по трафику.

Изображение

Как сделать sitemap.ts для блога и каталога инструментов

Файл app/sitemap.ts возвращает MetadataRoute.Sitemap с массивом URL, приоритетов и дат обновления. Для каталога с несколькими типами контента данные собираются из всех источников в один массив перед возвратом.

Для небольшого и среднего портала одного файла app/sitemap.ts достаточно. Он собирает статические страницы, все статьи блога и все карточки каталога в единый список.

// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getAllTools, getAllPosts, getAllCategories } from '@/lib/data';

const BASE_URL = 'https://vibecoderz.ru';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const [tools, posts, categories] = await Promise.all([
    getAllTools(),
    getAllPosts(),
    getAllCategories(),
  ]);

  const staticRoutes = [
    { url: BASE_URL, lastModified: new Date(), priority: 1.0, changeFrequency: 'daily' as const },
    { url: `${BASE_URL}/ide`, lastModified: new Date(), priority: 0.9, changeFrequency: 'weekly' as const },
    { url: `${BASE_URL}/agents`, lastModified: new Date(), priority: 0.8, changeFrequency: 'weekly' as const },
  ];

  const toolRoutes = tools.map((tool) => ({
    url: `${BASE_URL}/item/${tool.slug}`,
    lastModified: tool.updatedAt,
    priority: 0.9,
    changeFrequency: 'weekly' as const,
  }));

  const postRoutes = posts.map((post) => ({
    url: `${BASE_URL}/blog/${post.slug}`,
    lastModified: post.lastUpdated,
    priority: 0.8,
    changeFrequency: 'weekly' as const,
  }));

  const categoryRoutes = categories.map((cat) => ({
    url: `${BASE_URL}/category/${cat.slug}`,
    lastModified: new Date(),
    priority: 0.7,
    changeFrequency: 'monthly' as const,
  }));

  return [...staticRoutes, ...toolRoutes, ...postRoutes, ...categoryRoutes];
}

Важная деталь по lastModified: сюда идет реальная дата обновления записи из базы, а не текущая дата на каждый билд. Иначе Google видит, что все страницы "обновились" одновременно, и перестает доверять этому полю как сигналу свежести.

Когда нужен sitemap index и generateSitemaps

Один файл sitemap.xml ограничен 50 000 URL и 50 МБ несжатого текста. Если каталог превышает лимит, Next.js разбивает его на несколько файлов через generateSitemaps и автоматически собирает sitemap index.

Изображение

Пока в каталоге меньше 40-45 тысяч URL, обычный app/sitemap.ts работает без проблем. Но у порталов с товарным каталогом или тясячами материалов лимит подступает быстро. Здесь на помощь приходит generateSitemaps: функция возвращает массив идентификаторов чанков, а сама функция sitemap() получает id и отдает только свой кусок URL.

// app/item/sitemap.ts
import { MetadataRoute } from 'next';
import { db } from '@/lib/db';

const ITEMS_PER_SITEMAP = 50000;

export async function generateSitemaps() {
  const total = await db.item.count();
  const count = Math.ceil(total / ITEMS_PER_SITEMAP);
  return Array.from({ length: count }, (_, id) => ({ id }));
}

export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
  const items = await db.item.findMany({
    skip: id * ITEMS_PER_SITEMAP,
    take: ITEMS_PER_SITEMAP,
  });

  return items.map((item) => ({
    url: `https://vibecoderz.ru/item/${item.slug}`,
    lastModified: item.updatedAt,
  }));
}

Next.js сам генерирует файлы вида /item/sitemap/0.xml, /item/sitemap/1.xml и собирает индекс. По документации Next.js, этот механизм появился именно для разбиения крупных каталогов без ручной сборки XML. Google при этом поддерживает до 500 файлов в одном sitemap index, каждый до 50 000 записей, так что запаса хватает даже на очень крупный проект.

Максим: «Портал собрали за неделю тремя скриптами голосом в Claude Code, и на 6 200 материалах обычный sitemap.ts еще спокойно тянул все URL одним файлом. Sitemap index закладывали заранее, на рост. Дешевле сделать сразу с запасом, чем потом переписывать под нагрузкой.»

Что писать в robots.ts

Файл app/robots.ts управляет доступом краулеров к разделам сайта и указывает путь к sitemap. Закрывать нужно только служебные разделы: админку, API, черновики и личный кабинет.

Если sitemap.ts говорит роботам, куда идти, то robots.ts говорит, куда не идти. Для портала с каталогом и личным кабинетом типичный набор закрытых путей: /api, /dashboard, /studio (Sanity Studio), /auth и страницы с параметрами фильтров, которые дублируют контент.

// app/robots.ts
import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/api/', '/dashboard/', '/studio/', '/auth/', '/*?*sort='],
    },
    sitemap: 'https://vibecoderz.ru/sitemap.xml',
  };
}

Отдельно про AI-краулеров. GPTBot, ClaudeBot и PerplexityBot в 2026 году читают тот же robots.txt и тот же sitemap, что и Googlebot, чтобы решить, какие страницы цитировать в ответах. Если закрыть от них важные разделы каталога, страница физически не сможет попасть в AI Overview или в ответ Perplexity, даже если весь остальной SEO сделан идеально.

Изображение

Для инструмента в каталоге используется схема SoftwareApplication, для статьи блога BlogPosting, а для блока вопросов FAQPage. Без JSON-LD Google AI Overview обычно не подхватывает страницу, даже если контент по сути хороший.

На каждый тип контента своя схема, и смешивать их в одном компоненте не стоит: страница инструмента получает SoftwareApplication, страница блога BlogPosting, а FAQ-блок внутри любой страницы дополнительно оборачивается в FAQPage.

// components/schema/tool-schema.tsx
export function ToolSchema({ tool }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'SoftwareApplication',
    name: tool.name,
    description: tool.shortDescription,
    applicationCategory: 'DeveloperApplication',
    operatingSystem: 'Web, macOS, Windows',
    offers: {
      '@type': 'Offer',
      price: tool.priceFrom,
      priceCurrency: 'USD',
    },
    url: `https://vibecoderz.ru/item/${tool.slug}`,
  };

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

Для статей блога структура другая: BlogPosting с датами публикации и обновления, плюс отдельный блок FAQPage, если в статье есть раздел вопросов.

Компонент со схемой удобно вставлять прямо в generateMetadata рядом с обычными title и description, чтобы не забывать про него на новых страницах. Один пропущенный тип контента без schema, и именно эти страницы выпадают из AI-выдачи.

Изображение

Три типа маршрутов: сравнение подходов к генерации

Тип маршрутаСпособ генерацииКогда использоватьRevalidate
Статический (/pricing, /faq)Полная статика на билдеСтраницы, которые почти не меняютсяБез ISR
ISR через generateStaticParamsСтатика + фоновое обновлениеКарточки инструментов, статьи блога3600-86400 сек
Полностью динамический (route handler)Рендер на каждый запросЛичный кабинет, страницы с авторизациейНе применяется

Ошибка, которую чаще всего допускают на растущих порталах: весь каталог тянут через полную статику без ISR. При 200-300 карточках это незаметно, но при 3000+ билд начинает занимать 20+ минут, и каждое обновление одной карточки требует пересборки всего проекта. ISR с revalidate снимает эту проблему полностью.

Изображение

Итог: чеклист внедрения SEO-архитектуры

Перед запуском портала на Next.js стоит пройтись по короткому списку: маршруты разбиты по сегментам с собственным generateMetadata, sitemap.ts собирает все типы контента с реальными датами обновления, для каталога свыше 40 тысяч URL подключен generateSitemaps, robots.ts закрывает только служебные разделы и указывает путь к sitemap, а JSON-LD стоит на каждом типе страницы: SoftwareApplication для инструментов, BlogPosting для статей, FAQPage для блоков вопросов.

Готовые обзоры инструментов для вайбкодинга, включая Cursor и Windsurf, в каталоге vibecoderz.ru/ide собраны как раз по этой архитектуре: у каждой карточки своя schema, ISR-кеш и место в sitemap index.

Изображение

FAQ

Сколько URL помещается в один sitemap.xml в Next.js?
Лимит из спецификации sitemaps.org - 50 000 URL или 50 МБ несжатого файла. Если каталог больше, используйте generateSitemaps для разбиения на несколько файлов.

Нужен ли отдельный sitemap для каждого раздела портала?
Не обязательно. Один app/sitemap.ts в корне справляется, пока общее число URL не приближается к лимиту. Отдельные файлы по сегментам (app/item/sitemap.ts) имеет смысл делать, когда конкретный раздел растет быстрее остальных.

Что будет, если не поставить JSON-LD схему на страницу?
Страница все еще может индексироваться в обычном поиске, но не попадет в Google AI Overview и хуже цитируется в Perplexity и ChatGPT, потому что у них нет структурированного сигнала о типе контента.

Как часто обновлять revalidate для каталога инструментов?
Для карточек инструментов, где цены и статусы меняются нечасто, достаточно revalidate = 86400 (раз в сутки). Для новостного раздела ставят revalidate = 3600.

Можно ли закрыть от индексации страницы фильтров каталога?
Да, и стоит. Страницы вида /item?sort=price дублируют основной листинг и размывают crawl budget. Их закрывают в robots.ts через паттерн disallow.

Учитывают ли AI-краулеры robots.txt так же, как Googlebot?
Да. GPTBot, ClaudeBot и PerplexityBot в 2026 году читают тот же robots.txt и тот же sitemap, что и обычные поисковые боты, чтобы решить, какие страницы использовать для ответов.

Что делать, если билд с полной статикой стал слишком долгим?
Переводить основную часть каталога на ISR вместо предгенерации через generateStaticParams, оставляя предгенерацию только для топовых по трафику страниц.

Готовые схемы для вставки в generateMetadata этой страницы: BlogPosting описывает саму статью, FAQPage подтягивает блок вопросов выше для Google AI Overview и Perplexity.

Если нужна помощь с архитектурой конкретного портала, запишитесь на консультацию к Максиму.

Обновлено: октябрь 2026.

Автор

Елисавета Наговицына

Предприниматель · Контент-маркетолог · SEO-стратег · AI-продуктолог

2026/08/04

400 000+ органических переходов за 3 месяца. Со-основатель GoBanana (231K пользователей, 12+ млн ₽ без рекламы) и NeuroScribe (65K пользователей). SEO/GEO-стратегии для AI-поисковиков, 1 700+ единиц контента, 17+ реализованных стратегий.

Об авторе →
All Posts

Читать далее

📄 Статья

SEO каннибализация в блоге: как найти и устранить за один день

SEO каннибализация в блоге - это когда две или больше статей на сайте бьются за один и тот же запрос, и Google не понимает, какую из них показывать. В результате обе теряют позиции, а не растет ни одна. Ниже - пошаговый аудит на один день: как найти…

2026/08/049 мин
📄 Статья

GEO AEO SEO: простыми словами что делать блогеру и бизнесу прямо сейчас

GEO, AEO и SEO это три разных задачи, которые сегодня решаются одновременно. SEO отвечает за позиции сайта в обычной выдаче. AEO отвечает за попадание в быстрый ответ или голосовой ответ Алисы. GEO отвечает за упоминание бренда внутри развернутого от…

2026/08/049 мин
📄 Статья

AI Overviews и SEO: что изменить в статьях чтобы попадать в AI-ответы Яндекса

Яндекс Нейро уже обрабатывает 15% всех поисковых запросов с готовым AI-ответом, а в информационном сегменте доля нейроответов доходит до 68%. Если статья не устроена так, чтобы ИИ мог вырвать из нее кусок и процитировать, она проигрывает даже находяс…

2026/08/048 мин
📄 Статья

Programmatic SEO для AI портала: где масштаб оправдан и как не сделать мусор

Programmatic SEO для AI-портала работает только при двух условиях: есть повторяющийся паттерн данных и каждая страница несёт уникальный ценностный блок. Без второго условия масштаб превращается в тонкий контент, а с марта 2026 Google такие страницы р…

2026/08/047 мин
📄 Статья

Programmatic SEO ошибки 2026: 5 причин почему тысячи страниц не приносят трафик

Тысячи автоматически сгенерированных страниц с подстановкой города, продукта или ниши в 2026 году чаще топят сайт, чем поднимают его в выдаче. Ошибки programmatic SEO обычно сводятся к пяти вещам: тонкий контент, дубли с замененными переменными, отсу…

2026/08/048 мин
📄 Статья

Programmatic SEO шаблон страницы: переменные уникальные блоки и структура

Программатик SEO шаблон страницы — это конструкция из трёх слоёв: переменные из базы данных, блоки, которые генерирует AI по этим переменным, и блоки, написанные вручную. Без такого разделения тысячи страниц превращаются в копии друг друга с разными…

2026/08/047 мин