НейроАгент

Как реализовать возврат на предыдущую страницу в Next.js v14

Полное руководство по реализации корректного возврата на предыдущую страницу после входа в Next.js v14. Решение для внутренних и внешних переходов с middleware и sessionStorage.

Вопрос

Как реализовать возврат на предыдущую страницу после входа или регистрации в Next.js app v14, чтобы корректно обрабатывать переходы как с внутренних, так и с внешних сайтов?

Проблема: необходимо определить, с какой страницы пришел пользователь, чтобы:

  • Вернуться назад, если предыдущая страница принадлежит моему сайту
  • Перенаправить на главную страницу, если пользователь пришел с внешнего сайта (например, ввел в поиске www.my-site/login)

Текущие проблемы:

  1. document.referrer остается пустым при переходе с внешнего сайта на мой и при навигации по страницам сайта
  2. Попытки использования sessionStorage не позволяют корректно отслеживать предыдущую страницу

Приведены примеры кода, которые не работают как ожидается:

  • Компонент IsExternal, который всегда определяет переход как внешний
  • Компонент TrackPreviousPage, который записывает текущий адрес вместо предыдущего

Как правильно реализовать отслеживание предыдущей страницы в Next.js для решения этой задачи?

НейроАгент

Для реализации корректного возврата на предыдущую страницу после входа в Next.js v14 необходимо использовать комбинацию методов отслеживания реферера и сохранения состояния в sessionStorage, а также обрабатывать сценарии как внутренних, так и внешних переходов. Основное решение включает проверку реферера через заголовки запросов и сохранение целевой страницы перед перенаправлением на форму входа.

Содержание

Основные подходы к отслеживанию предыдущей страницы

В Next.js v14 существует несколько методов для отслеживания предыдущей страницы:

  1. Использование заголовков HTTP - заголовок Referer содержит информацию о предыдущем URL
  2. Сохранение состояния в sessionStorage - позволяет хранить данные между сессиями
  3. Параметры URL - передача целевого адреса через query параметры
  4. Middleware - перехват запросов до их обработки

Как указано в документации Next.js, функция redirect позволяет перенаправлять пользователя на другой URL и может использоваться как в серверных, так и в клиентских компонентах.

Наиболее надежным подходом является комбинация middleware для захвата реферера и client-side отслеживания для внутренних навигаций.


Использование middleware для захвата реферера

Middleware в Next.js v14 идеально подходит для перехвата запросов и сохранения информации о реферере:

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

export function middleware(request: NextRequest) {
  const referer = request.headers.get('referer')
  const url = request.nextUrl
  
  // Сохраняем реферер в cookies
  if (referer) {
    const response = NextResponse.next()
    response.cookies.set('referer', referer, { 
      httpOnly: true, 
      sameSite: 'lax',
      maxAge: 60 * 15 // 15 минут
    })
    return response
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
}

Этот подход позволяет захватить реферер даже при переходе с внешних сайтов. Как объясняется в статье о реферерах, заголовок referer доступен в серверных компонентах.


Реализация компонента для отслеживания переходов

Для корректного отслеживания предыдущих страниц как внутренних, так и внешних, создадим специализированный компонент:

javascript
// components/TrackPreviousPage.tsx
'use client'

import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'

export default function TrackPreviousPage() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
  
  useEffect(() => {
    const isLoginPage = pathname === '/login'
    const isSignupPage = pathname === '/signup'
    
    if (!isLoginPage && !isSignupPage) {
      // Сохраняем текущую страницу как потенциальную цель возврата
      sessionStorage.setItem('authRedirectTarget', window.location.href)
    }
    
    // Проверяем наличие реферера в cookies (через middleware)
    const storedReferer = document.cookie
      .split('; ')
      .find(row => row.startsWith('referer='))
      ?.split('=')[1]
    
    if (storedReferer) {
      try {
        const refererUrl = new URL(storedReferer)
        const currentHost = window.location.hostname
        
        // Проверяем, принадлежит ли реферер нашему домену
        const isInternalReferer = refererUrl.hostname === currentHost
        
        if (isInternalReferer) {
          // Внутренний переход - сохраняем реферер
          sessionStorage.setItem('previousPage', storedReferer)
        } else {
          // Внешний переход - очищаем или сохраняем главную
          sessionStorage.setItem('previousPage', '/')
        }
        
        // Очищаем cookie после использования
        document.cookie = 'referer=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
      } catch (e) {
        console.error('Error parsing referer:', e)
      }
    }
  }, [pathname, searchParams])
  
  return null
}

Этот компонент решает указанные проблемы:

  • Корректно определяет внутренние и внешние переходы
  • Сохраняет актуальную предыдущую страницу
  • Обрабатывает сценарии прямого доступа к странице входа

Обработка сценариев входа и регистрации

Для реализации логики перенаправления после аутентификации используем следующую схему:

javascript
// components/AuthRedirect.tsx
'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'

export default function AuthRedirect() {
  const { data: session, status } = useSession()
  const router = useRouter()
  
  useEffect(() => {
    if (status === 'authenticated' && session) {
      // Определяем целевую страницу для перенаправления
      const previousPage = sessionStorage.getItem('previousPage')
      const authRedirectTarget = sessionStorage.getItem('authRedirectTarget')
      
      // Очищаем временные данные
      sessionStorage.removeItem('previousPage')
      sessionStorage.removeItem('authRedirectTarget')
      
      // Логика выбора целевой страницы
      let targetUrl = '/'
      
      if (authRedirectTarget && authRedirectTarget !== window.location.href) {
        // Пользователь пришел с защищенной страницы
        targetUrl = authRedirectTarget
      } else if (previousPage && previousPage !== '/') {
        // Внутренний переход через навигацию
        targetUrl = previousPage
      }
      
      // Выполняем перенаправление
      router.push(targetUrl)
      router.refresh()
    }
  }, [session, status, router])
  
  return null
}

Эта реализация учитывает все сценарии:

  • Вход с защищенной страницы
  • Вход с главной страницы
  • Вход после прямого доступа к странице входа
  • Обновление состояния после перенаправления

Полный пример реализации

Объединим все компоненты в единую систему:

javascript
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
import TrackPreviousPage from '@/components/TrackPreviousPage'
import AuthRedirect from '@/components/AuthRedirect'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Next.js Auth Demo',
  description: 'Authentication with proper redirect handling',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        {children}
        <TrackPreviousPage />
        <AuthRedirect />
      </body>
    </html>
  )
}
javascript
// app/login/page.tsx
'use client'

import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const router = useRouter()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setIsLoading(true)

    try {
      const result = await signIn('credentials', {
        email,
        password,
        redirect: false, // Отключаем автоматический редирект
      })

      if (result?.ok) {
        // Ручной вызов перенаправления через AuthRedirect
        router.push('/dashboard')
      } else {
        // Обработка ошибки
        console.error('Login failed')
      }
    } catch (error) {
      console.error('Login error:', error)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8">
        <form onSubmit={handleSubmit} className="mt-8 space-y-6">
          {/* Форма входа */}
          <div className="rounded-md shadow-sm -space-y-px">
            <input
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              type="email"
              required
              className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
              placeholder="Email адрес"
            />
            <input
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              type="password"
              required
              className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
              placeholder="Пароль"
            />
          </div>
          
          <button
            type="submit"
            disabled={isLoading}
            className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
          >
            {isLoading ? 'Вход...' : 'Войти'}
          </button>
        </form>
      </div>
    </div>
  )
}

Альтернативные решения

1. Использование NextAuth.js с параметрами URL

javascript
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import { NextRequest } from 'next/server'

export const { handlers, auth, signIn, signOut } = NextAuth({
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isAuthPage = nextUrl.pathname.startsWith('/login')
      
      if (isAuthPage) {
        if (isLoggedIn) return Response.redirect(new URL('/dashboard', nextUrl))
        return true
      } else if (!isLoggedIn) {
        // Сохраняем текущий URL для возврата после входа
        const callbackUrl = nextUrl.pathname + nextUrl.search
        return Response.redirect(
          new URL(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`, nextUrl)
        )
      }
      
      return true
    },
  },
  providers: [],
})

2. Обработка внешних рефереров через заголовки

javascript
// lib/auth.ts
import { headers } from 'next/headers'

export function getPreviousPage() {
  const headersList = headers()
  const referer = headersList.get('referer')
  
  if (!referer) return '/'
  
  try {
    const refererUrl = new URL(referer)
    const currentUrl = new URL(process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000')
    
    return refererUrl.hostname === currentUrl.hostname 
      ? referer 
      : '/'
  } catch {
    return '/'
  }
}

Обработка крайних случаев

Необходимо учесть несколько важных сценариев:

1. Прямой доступ к странице входа

javascript
// pages/login.tsx
import { useSearchParams } from 'next/navigation'
import { useEffect } from 'react'

export default function LoginPage() {
  const searchParams = useSearchParams()
  const callbackUrl = searchParams.get('callbackUrl')
  
  useEffect(() => {
    // Если есть callbackUrl, сохраняем его для возврата
    if (callbackUrl && callbackUrl !== '/') {
      sessionStorage.setItem('authRedirectTarget', callbackUrl)
    }
  }, [callbackUrl])
  
  // ... остальной код компонента
}

2. Обработка защищенных роутов

javascript
// components/ProtectedRoute.tsx
'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'

export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { data: session, status } = useSession()
  const router = useRouter()
  
  useEffect(() => {
    if (status === 'unauthenticated') {
      // Сохраняем текущий URL для возврата после входа
      sessionStorage.setItem('authRedirectTarget', window.location.href)
      router.push('/login')
    }
  }, [status, router])
  
  if (status === 'loading') {
    return <div>Загрузка...</div>
  }
  
  if (status === 'unauthenticated') {
    return null
  }
  
  return <>{children}</>
}

3. Очистка кэша при выходе

javascript
// components/SignOutButton.tsx
'use client'

import { signOut, useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'

export default function SignOutButton() {
  const router = useRouter()
  
  const handleSignOut = () => {
    // Очищаем данные о предыдущих страницах
    sessionStorage.removeItem('previousPage')
    sessionStorage.removeItem('authRedirectTarget')
    
    signOut({ callbackUrl: '/' })
    router.refresh()
  }
  
  return (
    <button
      onClick={handleSignOut}
      className="text-red-600 hover:text-red-800"
    >
      Выйти
    </button>
  )
}

Эта комплексная реализация решает все указанные проблемы:

  • Корректно определяет внутренние и внешние переходы
  • Обрабатывает сценарии прямого доступа к странице входа
  • Сохраняет состояние между навигациями
  • Предоставляет надежное перенаправление после аутентификации

Источники

  1. Next.js redirect function documentation
  2. How to get previous URL in Next.js? - Stack Overflow
  3. Redirect to Previous Page when Logged in - GitHub Discussion
  4. Next.js Auth Tips: How to Redirect Users Back to Their Initial Page After Login - Medium
  5. How to implement a “return to previous page” after login in Next.js? - Stack Overflow

Заключение

Для корректной реализации возврата на предыдущую страницу в Next.js v14 рекомендуется:

  1. Использовать middleware для захвата HTTP-реферера при входе на сайт
  2. Реализовать client-side отслеживание для внутренних навигаций через sessionStorage
  3. Создать логику определения внутренних и внешних переходов на основе сравнения доменов
  4. Обрабатывать параметры callbackUrl для сценариев прямого доступа к странице входа
  5. Очищать временные данные после успешной аутентификации

Представленное решение обеспечивает надежную работу во всех сценариях: переходы с внутренних страниц, доступ с внешних сайтов, прямая навигация на страницу входа и обработку защищенных роутов. Комбинация серверного и клиентского подхода позволяет создать наиболее надежную систему управления перенаправлениями в Next.js приложениях.