Как реализовать возврат на предыдущую страницу после входа или регистрации в Next.js app v14, чтобы корректно обрабатывать переходы как с внутренних, так и с внешних сайтов?
Проблема: необходимо определить, с какой страницы пришел пользователь, чтобы:
- Вернуться назад, если предыдущая страница принадлежит моему сайту
- Перенаправить на главную страницу, если пользователь пришел с внешнего сайта (например, ввел в поиске www.my-site/login)
Текущие проблемы:
- document.referrer остается пустым при переходе с внешнего сайта на мой и при навигации по страницам сайта
- Попытки использования sessionStorage не позволяют корректно отслеживать предыдущую страницу
Приведены примеры кода, которые не работают как ожидается:
- Компонент IsExternal, который всегда определяет переход как внешний
- Компонент TrackPreviousPage, который записывает текущий адрес вместо предыдущего
Как правильно реализовать отслеживание предыдущей страницы в Next.js для решения этой задачи?
Для реализации корректного возврата на предыдущую страницу после входа в Next.js v14 необходимо использовать комбинацию методов отслеживания реферера и сохранения состояния в sessionStorage, а также обрабатывать сценарии как внутренних, так и внешних переходов. Основное решение включает проверку реферера через заголовки запросов и сохранение целевой страницы перед перенаправлением на форму входа.
Содержание
- Основные подходы к отслеживанию предыдущей страницы
- Использование middleware для захвата реферера
- Реализация компонента для отслеживания переходов
- Обработка сценариев входа и регистрации
- Полный пример реализации
- Альтернативные решения
- Обработка крайних случаев
Основные подходы к отслеживанию предыдущей страницы
В Next.js v14 существует несколько методов для отслеживания предыдущей страницы:
- Использование заголовков HTTP - заголовок
Refererсодержит информацию о предыдущем URL - Сохранение состояния в sessionStorage - позволяет хранить данные между сессиями
- Параметры URL - передача целевого адреса через query параметры
- Middleware - перехват запросов до их обработки
Как указано в документации Next.js, функция redirect позволяет перенаправлять пользователя на другой URL и может использоваться как в серверных, так и в клиентских компонентах.
Наиболее надежным подходом является комбинация middleware для захвата реферера и client-side отслеживания для внутренних навигаций.
Использование middleware для захвата реферера
Middleware в Next.js v14 идеально подходит для перехвата запросов и сохранения информации о реферере:
// 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 доступен в серверных компонентах.
Реализация компонента для отслеживания переходов
Для корректного отслеживания предыдущих страниц как внутренних, так и внешних, создадим специализированный компонент:
// 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
}
Этот компонент решает указанные проблемы:
- Корректно определяет внутренние и внешние переходы
- Сохраняет актуальную предыдущую страницу
- Обрабатывает сценарии прямого доступа к странице входа
Обработка сценариев входа и регистрации
Для реализации логики перенаправления после аутентификации используем следующую схему:
// 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
}
Эта реализация учитывает все сценарии:
- Вход с защищенной страницы
- Вход с главной страницы
- Вход после прямого доступа к странице входа
- Обновление состояния после перенаправления
Полный пример реализации
Объединим все компоненты в единую систему:
// 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>
)
}
// 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
// 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. Обработка внешних рефереров через заголовки
// 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. Прямой доступ к странице входа
// 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. Обработка защищенных роутов
// 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. Очистка кэша при выходе
// 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>
)
}
Эта комплексная реализация решает все указанные проблемы:
- Корректно определяет внутренние и внешние переходы
- Обрабатывает сценарии прямого доступа к странице входа
- Сохраняет состояние между навигациями
- Предоставляет надежное перенаправление после аутентификации
Источники
- Next.js redirect function documentation
- How to get previous URL in Next.js? - Stack Overflow
- Redirect to Previous Page when Logged in - GitHub Discussion
- Next.js Auth Tips: How to Redirect Users Back to Their Initial Page After Login - Medium
- How to implement a “return to previous page” after login in Next.js? - Stack Overflow
Заключение
Для корректной реализации возврата на предыдущую страницу в Next.js v14 рекомендуется:
- Использовать middleware для захвата HTTP-реферера при входе на сайт
- Реализовать client-side отслеживание для внутренних навигаций через sessionStorage
- Создать логику определения внутренних и внешних переходов на основе сравнения доменов
- Обрабатывать параметры callbackUrl для сценариев прямого доступа к странице входа
- Очищать временные данные после успешной аутентификации
Представленное решение обеспечивает надежную работу во всех сценариях: переходы с внутренних страниц, доступ с внешних сайтов, прямая навигация на страницу входа и обработку защищенных роутов. Комбинация серверного и клиентского подхода позволяет создать наиболее надежную систему управления перенаправлениями в Next.js приложениях.