diff --git a/package.json b/package.json index 074ae04..5f83d6f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@payloadcms/email-resend": "^3.84.1", "@payloadcms/live-preview-react": "^3.84.1", "@payloadcms/next": "^3.84.0", + "@payloadcms/plugin-redirects": "^3.84.1", "@payloadcms/plugin-seo": "^3.84.1", "@payloadcms/richtext-lexical": "^3.84.0", "@react-email/components": "^1.0.12", diff --git a/payload.config.ts b/payload.config.ts index 978b368..5877a3a 100644 --- a/payload.config.ts +++ b/payload.config.ts @@ -2,6 +2,7 @@ import { buildConfig } from 'payload' import { postgresAdapter } from '@payloadcms/db-postgres' import { lexicalEditor } from '@payloadcms/richtext-lexical' import { seoPlugin } from '@payloadcms/plugin-seo' +import { redirectsPlugin } from '@payloadcms/plugin-redirects' import { resendAdapter } from '@payloadcms/email-resend' import sharp from 'sharp' import path from 'path' @@ -89,6 +90,13 @@ export default buildConfig({ ], plugins: [ + redirectsPlugin({ + collections: ['pages'], + redirectTypes: ['301', '302'], + overrides: { + admin: { group: 'Контент сайту' }, + }, + }), seoPlugin({ collections: ['pages', 'blog-posts', 'locations'], globals: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e69c4d..6060369 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@payloadcms/next': specifier: ^3.84.0 version: 3.84.1(@types/react@19.2.14)(graphql@16.14.0)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.84.1(graphql@16.14.0)(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + '@payloadcms/plugin-redirects': + specifier: ^3.84.1 + version: 3.84.1(payload@3.84.1(graphql@16.14.0)(typescript@6.0.3)) '@payloadcms/plugin-seo': specifier: ^3.84.1 version: 3.84.1(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.84.1(graphql@16.14.0)(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3) @@ -1548,6 +1551,11 @@ packages: next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.2 <17.0.0' payload: 3.84.1 + '@payloadcms/plugin-redirects@3.84.1': + resolution: {integrity: sha512-izKKI5Mm6nosetsAxlqbdOKxraYPlHSGy246D4YbsDemAV4CLOpGcQKTaeTW/JAqYt+RlcvzcWXm0qNMX+2MeQ==} + peerDependencies: + payload: 3.84.1 + '@payloadcms/plugin-seo@3.84.1': resolution: {integrity: sha512-9FYs5ML/eWR/A/rQfHt2NhPzkJWbUx5SN/+lEQ90r3c3Z8CQUVpt4vETXSI9Gxi764lTutIGemuZAJK9WRy3Lw==} peerDependencies: @@ -6988,6 +6996,11 @@ snapshots: - supports-color - typescript + '@payloadcms/plugin-redirects@3.84.1(payload@3.84.1(graphql@16.14.0)(typescript@6.0.3))': + dependencies: + '@payloadcms/translations': 3.84.1 + payload: 3.84.1(graphql@16.14.0)(typescript@6.0.3) + '@payloadcms/plugin-seo@3.84.1(@types/react@19.2.14)(monaco-editor@0.55.1)(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(payload@3.84.1(graphql@16.14.0)(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3)': dependencies: '@payloadcms/translations': 3.84.1 diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..13a504f --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,47 @@ +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' + +let redirectsCache: { from: string; to: string; type: string }[] | null = null +let cacheExpiry = 0 + +async function getRedirects(siteURL: string) { + if (redirectsCache && Date.now() < cacheExpiry) return redirectsCache + + try { + const res = await fetch(`${siteURL}/api/redirects?limit=100&depth=1`, { + next: { revalidate: 300 }, + }) + if (!res.ok) return [] + const data = (await res.json()) as { + docs: { from: string; to: { url?: string }; redirectType: string }[] + } + redirectsCache = data.docs.map((r) => ({ + from: r.from, + to: r.to?.url ?? '/', + type: r.redirectType, + })) + cacheExpiry = Date.now() + 5 * 60 * 1000 + return redirectsCache + } catch { + return [] + } +} + +export async function middleware(request: NextRequest) { + const siteURL = process.env['NEXT_PUBLIC_SITE_URL'] ?? 'http://localhost:3000' + const pathname = request.nextUrl.pathname + + const redirects = await getRedirects(siteURL) + const match = redirects.find((r) => r.from === pathname) + + if (match) { + const status = match.type === '302' ? 302 : 301 + return NextResponse.redirect(new URL(match.to, request.url), status) + } + + return NextResponse.next() +} + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico|api/|admin/).*)'], +}