Как обрабатывать редиректы в Next.js

Существует несколько способов обработки редиректов в Next.js. На этой странице рассматриваются все доступные варианты, случаи их использования и управление большим количеством редиректов.

APIНазначениеГде используетсяКод состояния
useRouterКлиентская навигацияКомпонентыN/A
redirects в next.config.jsПеренаправление входящего запроса на основе путиФайл next.config.js307 (Временный) или 308 (Постоянный)
NextResponse.redirectПеренаправление входящего запроса на основе условияMiddlewareЛюбой

Хук useRouter()

Если нужно перенаправить пользователя внутри компонента, можно использовать метод push из хука useRouter. Например:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

Важно знать:

  • Если не требуется программная навигация, лучше использовать компонент <Link>.

Подробнее см. в справочнике API useRouter.

redirects в next.config.js

Опция redirects в файле next.config.js позволяет перенаправлять входящие запросы с одного пути на другой. Это полезно при изменении структуры URL страниц или наличии заранее известного списка редиректов.

redirects поддерживает сопоставление путей, заголовков, куки и запросов, что даёт гибкость в перенаправлении пользователей на основе входящего запроса.

Чтобы использовать redirects, добавьте опцию в файл next.config.js:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async redirects() {
    return [
      // Простой редирект
      {
        source: '/about',
        destination: '/',
        permanent: true,
      },
      // Сопоставление с подстановочным знаком
      {
        source: '/blog/:slug',
        destination: '/news/:slug',
        permanent: true,
      },
    ]
  },
}

export default nextConfig
module.exports = {
  async redirects() {
    return [
      // Простой редирект
      {
        source: '/about',
        destination: '/',
        permanent: true,
      },
      // Сопоставление с подстановочным знаком
      {
        source: '/blog/:slug',
        destination: '/news/:slug',
        permanent: true,
      },
    ]
  },
}

Подробнее см. в справочнике API redirects.

Важно знать:

  • redirects может возвращать код состояния 307 (Временное перенаправление) или 308 (Постоянное перенаправление) с опцией permanent.
  • На некоторых платформах есть ограничения на количество редиректов. Например, на Vercel лимит составляет 1024 редиректа. Для управления большим количеством редиректов (1000+) рассмотрите создание кастомного решения с использованием Middleware. Подробнее см. в разделе управление большим количеством редиректов.
  • redirects выполняется до Middleware.

NextResponse.redirect в Middleware

Middleware позволяет выполнять код до завершения запроса. Затем, на основе входящего запроса, можно перенаправить пользователя на другой URL с помощью NextResponse.redirect. Это полезно для перенаправления пользователей на основе условий (например, аутентификации, управления сессиями) или при большом количестве редиректов.

Например, для перенаправления пользователя на страницу /login, если он не аутентифицирован:

import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'

export function middleware(request: NextRequest) {
  const isAuthenticated = authenticate(request)

  // Если пользователь аутентифицирован, продолжить как обычно
  if (isAuthenticated) {
    return NextResponse.next()
  }

  // Перенаправление на страницу входа, если не аутентифицирован
  return NextResponse.redirect(new URL('/login', request.url))
}

export const config = {
  matcher: '/dashboard/:path*',
}
import { NextResponse } from 'next/server'
import { authenticate } from 'auth-provider'

export function middleware(request) {
  const isAuthenticated = authenticate(request)

  // Если пользователь аутентифицирован, продолжить как обычно
  if (isAuthenticated) {
    return NextResponse.next()
  }

  // Перенаправление на страницу входа, если не аутентифицирован
  return NextResponse.redirect(new URL('/login', request.url))
}

export const config = {
  matcher: '/dashboard/:path*',
}

Важно знать:

  • Middleware выполняется после redirects в next.config.js и до рендеринга.

Подробнее см. в документации по Middleware.

Управление большим количеством редиректов (продвинутый уровень)

Для управления большим количеством редиректов (1000+) можно создать кастомное решение с использованием Middleware. Это позволяет обрабатывать редиректы программно без необходимости переразвёртывания приложения.

Для этого нужно учитывать:

  1. Создание и хранение карты редиректов.
  2. Оптимизацию производительности поиска данных.

Пример Next.js: Смотрите наш пример Middleware с фильтром Блума для реализации рекомендаций ниже.

1. Создание и хранение карты редиректов

Карта редиректов — это список редиректов, который можно хранить в базе данных (обычно key-value хранилище) или JSON-файле.

Рассмотрим следующую структуру данных:

{
  "/old": {
    "destination": "/new",
    "permanent": true
  },
  "/blog/post-old": {
    "destination": "/blog/post-new",
    "permanent": true
  }
}

В Middleware можно читать из базы данных, например Edge Config от Vercel или Redis, и перенаправлять пользователя на основе входящего запроса:

import { NextResponse, NextRequest } from 'next/server'
import { get } from '@vercel/edge-config'

type RedirectEntry = {
  destination: string
  permanent: boolean
}

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname
  const redirectData = await get(pathname)

  if (redirectData && typeof redirectData === 'string') {
    const redirectEntry: RedirectEntry = JSON.parse(redirectData)
    const statusCode = redirectEntry.permanent ? 308 : 307
    return NextResponse.redirect(redirectEntry.destination, statusCode)
  }

  // Редирект не найден, продолжить без перенаправления
  return NextResponse.next()
}
import { NextResponse } from 'next/server'
import { get } from '@vercel/edge-config'

export async function middleware(request) {
  const pathname = request.nextUrl.pathname
  const redirectData = await get(pathname)

  if (redirectData) {
    const redirectEntry = JSON.parse(redirectData)
    const statusCode = redirectEntry.permanent ? 308 : 307
    return NextResponse.redirect(redirectEntry.destination, statusCode)
  }

  // Редирект не найден, продолжить без перенаправления
  return NextResponse.next()
}

2. Оптимизация производительности поиска данных

Чтение большого набора данных для каждого входящего запроса может быть медленным и ресурсоемким. Есть два способа оптимизировать производительность поиска данных:

  • Использовать базу данных, оптимизированную для быстрого чтения
  • Использовать стратегию поиска данных, такую как фильтр Блума (Bloom filter), чтобы эффективно проверить наличие редиректа перед чтением большого файла или базы данных с редиректами.

Рассмотрим предыдущий пример: можно импортировать сгенерированный файл фильтра Блума в Middleware, а затем проверить, существует ли путь входящего запроса в фильтре.

Если путь существует, запрос перенаправляется в API-маршруты (API Routes), который проверит фактический файл и перенаправит пользователя на нужный URL. Это позволяет избежать импорта большого файла редиректов в Middleware, что может замедлить обработку каждого входящего запроса.

import { NextResponse, NextRequest } from 'next/server'
import { ScalableBloomFilter } from 'bloom-filters'
import GeneratedBloomFilter from './redirects/bloom-filter.json'

type RedirectEntry = {
  destination: string
  permanent: boolean
}

// Инициализация фильтра Блума из сгенерированного JSON-файла
const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter as any)

export async function middleware(request: NextRequest) {
  // Получаем путь входящего запроса
  const pathname = request.nextUrl.pathname

  // Проверяем, есть ли путь в фильтре Блума
  if (bloomFilter.has(pathname)) {
    // Перенаправляем путь в Обработчик маршрута
    const api = new URL(
      `/api/redirects?pathname=${encodeURIComponent(request.nextUrl.pathname)}`,
      request.nextUrl.origin
    )

    try {
      // Получаем данные редиректа из Обработчика маршрута
      const redirectData = await fetch(api)

      if (redirectData.ok) {
        const redirectEntry: RedirectEntry | undefined =
          await redirectData.json()

        if (redirectEntry) {
          // Определяем код статуса
          const statusCode = redirectEntry.permanent ? 308 : 307

          // Перенаправляем на целевой URL
          return NextResponse.redirect(redirectEntry.destination, statusCode)
        }
      }
    } catch (error) {
      console.error(error)
    }
  }

  // Редирект не найден, продолжаем обработку запроса без перенаправления
  return NextResponse.next()
}
import { NextResponse } from 'next/server'
import { ScalableBloomFilter } from 'bloom-filters'
import GeneratedBloomFilter from './redirects/bloom-filter.json'

// Инициализация фильтра Блума из сгенерированного JSON-файла
const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter)

export async function middleware(request) {
  // Получаем путь входящего запроса
  const pathname = request.nextUrl.pathname

  // Проверяем, есть ли путь в фильтре Блума
  if (bloomFilter.has(pathname)) {
    // Перенаправляем путь в Обработчик маршрута
    const api = new URL(
      `/api/redirects?pathname=${encodeURIComponent(request.nextUrl.pathname)}`,
      request.nextUrl.origin
    )

    try {
      // Получаем данные редиректа из Обработчика маршрута
      const redirectData = await fetch(api)

      if (redirectData.ok) {
        const redirectEntry = await redirectData.json()

        if (redirectEntry) {
          // Определяем код статуса
          const statusCode = redirectEntry.permanent ? 308 : 307

          // Перенаправляем на целевой URL
          return NextResponse.redirect(redirectEntry.destination, statusCode)
        }
      }
    } catch (error) {
      console.error(error)
    }
  }

  // Редирект не найден, продолжаем обработку запроса без перенаправления
  return NextResponse.next()
}

Затем в API-маршруте:

import type { NextApiRequest, NextApiResponse } from 'next'
import redirects from '@/app/redirects/redirects.json'

type RedirectEntry = {
  destination: string
  permanent: boolean
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const pathname = req.query.pathname
  if (!pathname) {
    return res.status(400).json({ message: 'Bad Request' })
  }

  // Получаем запись редиректа из файла redirects.json
  const redirect = (redirects as Record<string, RedirectEntry>)[pathname]

  // Учитываем ложные срабатывания фильтра Блума
  if (!redirect) {
    return res.status(400).json({ message: 'No redirect' })
  }

  // Возвращаем запись редиректа
  return res.json(redirect)
}
import redirects from '@/app/redirects/redirects.json'

export default function handler(req, res) {
  const pathname = req.query.pathname
  if (!pathname) {
    return res.status(400).json({ message: 'Bad Request' })
  }

  // Получаем запись редиректа из файла redirects.json
  const redirect = redirects[pathname]

  // Учитываем ложные срабатывания фильтра Блума
  if (!redirect) {
    return res.status(400).json({ message: 'No redirect' })
  }

  // Возвращаем запись редиректа
  return res.json(redirect)
}

Полезно знать:

  • Для генерации фильтра Блума можно использовать библиотеку bloom-filters.
  • Следует проверять запросы к вашему Обработчику маршрута, чтобы предотвратить вредоносные запросы.