Middleware Next.js 15: 301-редиректы без потери SEO — KEL IT
Сайты 9 мин чтения

Middleware в Next.js 15: 301-редиректы, trailing slash и миграция URL без потери SEO

Лендинг выходит в прод на Vercel. Через неделю в Search Console — 47 URL с «Страница с переадресацией», 12 дублей /pricing и /pricing/, а старые ссылки с /services/web-dev ведут на 404. Canonical вы прописали в metadata, sitemap собрали, JSON-LD добавили — но краулер всё равно обходит мусорные варианты URL, тратит crawl budget и размывает сигнал на каноническую страницу.

Проблема не в контенте и не в Core Web Vitals. Проблема в том, что редиректы на уровне CDN или nginx настроены частично, а App Router по умолчанию не знает про ваши legacy-пути. next.config.js redirects работают, но не покрывают динамические кейсы: A/B-префиксы, гео-поддомены, нормализацию регистра в query string.

Middleware в Next.js 15 — edge-функция, которая выполняется до рендера страницы. Она идеальна для SEO-инфраструктуры лендинга: один источник правды для 301/308, trailing slash, www → apex, http → https (если не делает Vercel автоматически), редирект старых URL после редизайна.

В этой статье — production-паттерны middleware для SEO-лендинга на Next.js 15: архитектура, код, типичные ловушки и проверка в Search Console.

Почему редиректы — часть SEO, а не «настройка сервера»

Google индексирует URL как идентификатор документа. Два URL с одинаковым контентом — два кандидата в индекс. Canonical подсказывает предпочтительный вариант, но:

  • краулер всё равно заходит на оба URL и тратит quota;
  • внешние ссылки могут указывать на «неправильный» вариант;
  • при смене структуры без 301 ссылочный вес не передаётся на новый адрес.
СитуацияБез middlewareС 301 на edge
/about/ и /aboutДва URL в индексеОдин канонический
Старый /blog/post-slug после миграции404, потеря трафика301 → /articles/post-slug
www.example.comДубль apex-домена301 → example.com
/Pricing (регистр)Редкий, но возможный дубль301 → /pricing

Для коммерческого лендинга с 5–15 страницами crawl budget не критичен. Для programmatic SEO (100+ landing pages по городам или нишам) — каждый лишний URL заметен. Middleware масштабируется: одна функция на все маршруты, latency < 5 ms на Vercel Edge.

301 vs 308: для SEO постоянного переноса используйте 301 (или 308 для POST-safe редиректов). 302/307 говорят «временно» — Google может не передать весь сигнал. Next.js NextResponse.redirect(url, 301) — стандарт для миграций.

Базовая структура middleware.ts в App Router

Файл middleware.ts лежит в корне проекта (рядом с app/). Matcher ограничивает, на каких путях функция запускается — исключайте /_next, статику, API webhooks.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();
  const { pathname, search } = url;
  const host = request.headers.get('host') ?? '';

  // 1. www → apex (настройте и DNS, и canonical одинаково)
  if (host.startsWith('www.')) {
    url.host = host.replace(/^www\./, '');
    return NextResponse.redirect(url, 301);
  }

  // 2. Trailing slash: единый стиль без слэша (или наоборот — главное consistency)
  if (pathname !== '/' && pathname.endsWith('/')) {
    url.pathname = pathname.slice(0, -1);
    return NextResponse.redirect(url, 301);
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    /*
     * Все пути кроме:
     * - api (если не нужны редиректы)
     * - _next/static, _next/image
     * - favicon, robots, sitemap (опционально)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)',
  ],
};

Ключевые моменты:

  • request.nextUrl.clone() — не мутируйте оригинал напрямую; клонируйте и меняйте pathname/host.
  • Matcher — без него middleware бежит на каждый asset и добавляет latency. Regex выше — распространённый шаблон из документации Next.js.
  • Edge Runtime — middleware всегда на edge, не на Node.js server. Нет доступа к файловой системе и некоторым Node API — карта редиректов держите в коде, Edge Config или KV.

Согласованность с metadata и sitemap

Если middleware убирает trailing slash, то:

  • в generateMetadata canonical без слэша: https://example.com/pricing;
  • в sitemap.ts — те же URL;
  • внутренние <Link href="/pricing"> — без слэша.

Три источника, один формат. Расхождение canonical vs фактический URL после редиректа — warning в Search Console.

Если нужна подобная разработка — напишите в Telegram.

Миграция legacy URL: карта 301 без next.config bloat

После редизайна лендинга старые URL из Google и бэклинков должны жить. redirects() в next.config.ts подходит для десятка правил; для сотен — неудобно пересобирать проект при каждом добавлении.

Статическая карта в middleware

// lib/legacy-redirects.ts
const LEGACY_REDIRECTS: Record<string, string> = {
  '/services/web-development': '/services/websites',
  '/services/web-dev': '/services/websites',
  '/portfolio': '/cases',
  '/blog': '/articles',
  // slug-level — отдельный объект или regex
};

export function getLegacyRedirect(pathname: string): string | null {
  return LEGACY_REDIRECTS[pathname] ?? null;
}
// middleware.ts (фрагмент)
import { getLegacyRedirect } from '@/lib/legacy-redirects';

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();
  const legacy = getLegacyRedirect(url.pathname);

  if (legacy) {
    url.pathname = legacy;
    return NextResponse.redirect(url, 301);
  }

  // ... остальные правила
}

Query string: при редиректе UTM-метки часто нужно сохранить. NextResponse.redirect с url.search из clone — параметры переносятся автоматически, если вы не обнуляете search.

Динамические slug: regex-правила

Старый формат /blog/2024/my-post → новый /articles/my-post:

const BLOG_LEGACY = /^\/blog\/(\d{4})\/(.+)$/;

function matchBlogLegacy(pathname: string): string | null {
  const m = pathname.match(BLOG_LEGACY);
  if (!m) return null;
  return `/articles/${m[2]}`;
}

Порядок правил важен: сначала точные совпадения, потом regex, потом trailing slash. Иначе /blog/2024/post/ может пройти slash-нормализацию до legacy-match.

Edge Config для маркетинга без деплоя

Команда маркетинга просит редирект /promo-winter/pricing?ref=winter без релиза. Vercel Edge Config хранит JSON-карту; middleware читает её на edge с кешем.

import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  const promos = await get<Record<string, string>>('redirects');
  const target = promos?.[request.nextUrl.pathname];
  if (target) {
    return NextResponse.redirect(new URL(target, request.url), 302); // временная акция — 302
  }
  // ...
}

Для постоянной миграции URL — 301 в коде с code review. Для кампаний — 302 из Edge Config.

Локализация, регистр и опасные паттерны

Locale prefix без дублей

Мультиязычный лендинг: /ru/pricing и /en/pricing. Middleware может:

  • определять язык по Accept-Language и редиректить /pricing/ru/pricing (осторожно с SEO — лучше явные hreflang, см. отдельную статью);
  • блокировать несуществующие локали: /xx/pricing → 301 на /en/pricing.

Не смешивайте авто-редирект по IP с индексацией: Googlebot из США может не увидеть /ru/, если редиректите всех на /en/. Для ботов — NextResponse.next() без гео-редиректа или отдельная логика по User-Agent (Google рекомендует один URL для всех).

Lowercase pathname

На case-sensitive файловых системах /About и /about — разные маршруты. Нормализация:

const lower = pathname.toLowerCase();
if (pathname !== lower) {
  url.pathname = lower;
  return NextResponse.redirect(url, 301);
}

Исключите API и пути с case-sensitive ID, если они есть.

Чего не делать

АнтипаттернРиск
Цепочка 301 → 301 → 301Crawl budget, потеря ~15% сигнала на каждый hop
301 на страницу, которая сама 301Loop, URL excluded from index
Редирект / на /homeГлавная должна быть /
Middleware с тяжёлым fetch на каждый запросTTFB растёт, страдает LCP
302 вместо 301 при смене slug навсегдаGoogle может индексировать старый URL

Правило: максимум один hop от любого legacy URL до финального канонического.

Проверка: curl, Lighthouse, Search Console

Локальная и staging-проверка

# Статус и Location header
curl -I https://staging.example.com/services/web-dev

# Ожидаем: HTTP/2 301, location: /services/websites

Проверьте цепочку:

curl -IL https://www.example.com/Pricing/
# Должен закончиться одним 200 на https://example.com/pricing

Флаг -L следует редиректам; если больше двух 301 — упростите правила.

Search Console после деплоя

  1. Страницы → Исключено → «Страница с переадресацией» — нормально для legacy URL; плохо, если канонический URL сам в этом статусе.
  2. Проверка URL — «URL доступен Google» для нового адреса; для старого — «URL перенаправляет».
  3. Sitemap — только финальные URL без редиректов.

Vercel Analytics и логи

В Vercel → Logs фильтр status:301 покажет топ редиректов. Если /undefined или странные query — баг в middleware. Speed Insights не страдает от лёгкого middleware, но тяжёлый Edge Config fetch на cold start добавляет десятки ms — кешируйте.

Интеграция с next.config redirects

Дублируйте правила либо в middleware, либо в config — не в обоих. next.config redirects выполняются на другом уровне; двойной 301 возможен при ошибке. Для SEO-лендинга предпочтите middleware (гибкость, Edge Config) и держите в config только images, headers.

Нужна помощь? Telegram → или vic.kell@ya.ru

FAQ

Middleware или redirects в next.config — что выбрать?

redirects в config — декларативно, без логики, пересборка при изменении. Middleware — условная логика, regex, Edge Config, один hop на edge. Для SEO-миграций и нормализации URL выбирайте middleware; для пары постоянных правил достаточно config.

Trailing slash: со слэшем или без?

Google не penalize ни тот, ни другой вариант — важна консistency. Next.js по умолчанию без trailing slash. Выберите стиль, пропишите в middleware, canonical, sitemap и <Link>. Не смешивайте.

Редирект http → https нужен в middleware?

На Vercel HTTPS принудительный на уровне платформы. В middleware дублировать не обязательно. На self-hosted — да, первое правило в middleware или reverse proxy.

Влияет ли middleware на Core Web Vitals?

Лёгкая функция (< 1 ms CPU) на edge не влияет на LCP/INP. Тяжёлые await-fetch, JWT-verify на каждый статический asset — влияют на TTFB. Ограничивайте matcher.

Как редиректить только для Googlebot?

Не рекомендуется (cloaking). Все пользователи и боты должны видеть одинаковые 301. Исключение — технические URL (/wp-admin/), не контентные варианты.

301 и A/B-тесты на разных URL

Если тестируете /pricing-a vs /pricing-b как отдельные URL — оба могут попасть в индекс. Для SEO используйте один URL и client-side или server-side split без индексации вариантов (noindex на альтернативах) или canonical на победителя после теста.

Заключение

SEO-лендинг на Next.js 15 — это не только metadata и Core Web Vitals. Чистая URL-архитектура через middleware на edge предотвращает дубли, сохраняет ссылочный вес при миграциях и снижает шум в Search Console. Один файл middleware.ts с matcher, картой legacy-редиректов и единым правилом trailing slash согласуется с canonical и sitemap — и работает на Vercel за миллисекунды.

Перед каждым редизайном структуры соберите таблицу старых URL из Analytics и Search Console, покройте 301, проверьте цепочки через curl -IL и обновите sitemap только финальными адресами. Так миграция не обнуляет месяцы SEO-работы — даже если hero, шрифты и JSON-LD уже настроены идеально.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.