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

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

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

Функция redirect

Функция redirect позволяет перенаправить пользователя на другой URL. Её можно вызывать в Серверных компонентах, Обработчиках маршрутов и Серверных действиях.

redirect часто используется после мутации или события. Например, при создании поста:

'use server'

import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function createPost(id: string) {
  try {
    // Вызов базы данных
  } catch (error) {
    // Обработка ошибок
  }

  revalidatePath('/posts') // Обновление кэшированных постов
  redirect(`/post/${id}`) // Переход на страницу нового поста
}
'use server'

import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function createPost(id) {
  try {
    // Вызов базы данных
  } catch (error) {
    // Обработка ошибок
  }

  revalidatePath('/posts') // Обновление кэшированных постов
  redirect(`/post/${id}`) // Переход на страницу нового поста
}

Важно знать:

  • По умолчанию redirect возвращает код состояния 307 (Временное перенаправление). При использовании в Серверном действии возвращается код 303 (Смотреть другое), который обычно применяется для перенаправления на страницу успеха после POST-запроса.
  • redirect внутренне выбрасывает ошибку, поэтому должен вызываться вне блоков try/catch.
  • redirect можно вызывать в Клиентских компонентах во время рендеринга, но не в обработчиках событий. Вместо этого используйте хук useRouter.
  • redirect также принимает абсолютные URL и может использоваться для перенаправления на внешние ссылки.
  • Если нужно перенаправить до процесса рендеринга, используйте next.config.js или Middleware.

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

Функция permanentRedirect

Функция permanentRedirect позволяет постоянно перенаправить пользователя на другой URL. Её можно вызывать в Серверных компонентах, Обработчиках маршрутов и Серверных действиях.

permanentRedirect часто используется после мутации или события, изменяющего канонический URL сущности, например, при обновлении URL профиля пользователя после смены имени:

'use server'

import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function updateUsername(username: string, formData: FormData) {
  try {
    // Вызов базы данных
  } catch (error) {
    // Обработка ошибок
  }

  revalidateTag('username') // Обновление всех ссылок на имя пользователя
  permanentRedirect(`/profile/${username}`) // Переход на новый профиль
}
'use server'

import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function updateUsername(username, formData) {
  try {
    // Вызов базы данных
  } catch (error) {
    // Обработка ошибок
  }

  revalidateTag('username') // Обновление всех ссылок на имя пользователя
  permanentRedirect(`/profile/${username}`) // Переход на новый профиль
}

Важно знать:

  • permanentRedirect возвращает код состояния 308 (Постоянное перенаправление) по умолчанию.
  • permanentRedirect также принимает абсолютные URL и может использоваться для перенаправления на внешние ссылки.
  • Если нужно перенаправить до процесса рендеринга, используйте next.config.js или Middleware.

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

Хук useRouter()

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

'use client'

import { useRouter } from 'next/navigation'

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

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

import { useRouter } from 'next/navigation'

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, а затем проверить, существует ли путь входящего запроса в фильтре.

Если путь существует, запрос перенаправляется в Обработчик маршрута (Route Handler) , который проверит фактический файл и перенаправит пользователя на нужный 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()
}

Затем в Обработчике маршрута:

import { NextRequest, NextResponse } from 'next/server'
import redirects from '@/app/redirects/redirects.json'

type RedirectEntry = {
  destination: string
  permanent: boolean
}

export function GET(request: NextRequest) {
  const pathname = request.nextUrl.searchParams.get('pathname')
  if (!pathname) {
    return new Response('Bad Request', { status: 400 })
  }

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

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

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

export function GET(request) {
  const pathname = request.nextUrl.searchParams.get('pathname')
  if (!pathname) {
    return new Response('Bad Request', { status: 400 })
  }

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

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

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

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

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