From 9a55b874a54d80bebe60eb6a8200f843ed15cfd7 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 28 Apr 2026 09:22:18 +0800 Subject: [PATCH] Use CMS redirects for support tutorials --- REDIRECTS.md | 4 +- docs/production-release-checklist.md | 1 + next.config.mjs | 2 - src/app/(payload)/admin-theme.css | 25 +++++++++ .../admin/_components/eversolo-admin-logo.tsx | 7 +++ src/app/(payload)/admin/importMap.js | 4 ++ src/app/Support/tutorial.html/route.ts | 9 ++++ .../Support/tutorial/target/[target]/route.ts | 9 ++++ src/lib/cms-redirect.ts | 45 ++++++++++++++++ ..._011500_seed_support_tutorial_redirects.ts | 52 +++++++++++++++++++ src/migrations/index.ts | 6 +++ src/payload.config.ts | 10 ++++ src/scripts/smoke-check.mjs | 3 ++ src/seed/import-seed.mjs | 2 + 14 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/app/(payload)/admin/_components/eversolo-admin-logo.tsx create mode 100644 src/app/Support/tutorial.html/route.ts create mode 100644 src/app/Support/tutorial/target/[target]/route.ts create mode 100644 src/lib/cms-redirect.ts create mode 100644 src/migrations/20260428_011500_seed_support_tutorial_redirects.ts diff --git a/REDIRECTS.md b/REDIRECTS.md index e4d576a..77dd84a 100644 --- a/REDIRECTS.md +++ b/REDIRECTS.md @@ -85,8 +85,8 @@ Locale is inferred from the user's current site locale, not from the legacy URL ## Footer / Secondary Links | Legacy | New | Code | |---|---|---| -| `/Support/tutorial.html` | `/{locale}/support/tutorial` | 301 | -| `/Support/tutorial/target/.html` | `/{locale}/support/tutorial` | 301 | +| `/Support/tutorial.html` | `/{locale}/support/tutorial` | 301, managed in CMS Redirects | +| `/Support/tutorial/target/.html` | `/{locale}/support/tutorial` | 301, managed in CMS Redirects | | `/Contact/afterservice.html` | `/{locale}/support/warranty` | 301 | | `/Contact/distributor.html` | `/{locale}/dealers` | 301 | | `/Contact/oem.html` | `/{locale}/support/contact` | 301 | diff --git a/docs/production-release-checklist.md b/docs/production-release-checklist.md index 5b8407f..0b7866c 100644 --- a/docs/production-release-checklist.md +++ b/docs/production-release-checklist.md @@ -13,6 +13,7 @@ Use this checklist before every production deployment. - [ ] `npm run payload:migrate:status` shows all migrations as `Yes` on the release database. - [ ] `payload_migrations` has no `batch = -1` dev marker before exporting/importing the database. - [ ] Admin login shows a verification code, invalid captcha fails before password validation, and `/admin` redirects to `/admin/login` when logged out. +- [ ] Legacy tutorial redirects are present in CMS Redirects, not in `next.config.mjs`. - [ ] No new user-facing fallback copy was introduced without a CMS field. ## Environment diff --git a/next.config.mjs b/next.config.mjs index 3841253..a1fdd73 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -28,8 +28,6 @@ const nextConfig = { { source: '/About/index.html', destination: '/en/about', permanent: true }, // Footer / secondary - { source: '/Support/tutorial.html', destination: '/en/support/tutorial', permanent: true }, - { source: '/Support/tutorial/target/:target.html', destination: '/en/support/tutorial', permanent: true }, { source: '/Contact/afterservice.html', destination: '/en/support/warranty', permanent: true }, { source: '/Contact/distributor.html', destination: '/en/dealers', permanent: true }, { source: '/Contact/oem.html', destination: '/en/support/contact', permanent: true }, diff --git a/src/app/(payload)/admin-theme.css b/src/app/(payload)/admin-theme.css index 6d38115..422e291 100644 --- a/src/app/(payload)/admin-theme.css +++ b/src/app/(payload)/admin-theme.css @@ -1118,6 +1118,31 @@ html[data-theme] .eversolo-login-logo { text-transform: uppercase; } +html[data-theme] .eversolo-admin-logo, +html[data-theme] .eversolo-admin-icon { + display: inline-grid; + place-items: center; + border-radius: 10px; + background: linear-gradient(135deg, #2f241b, #54402f); + color: #fff9f1; + font-weight: 700; + text-transform: uppercase; +} + +html[data-theme] .eversolo-admin-logo { + min-width: 150px; + padding: 13px 18px; + font-size: 16px; + letter-spacing: 0.24em; +} + +html[data-theme] .eversolo-admin-icon { + width: 34px; + height: 34px; + font-size: 13px; + letter-spacing: 0.08em; +} + html[data-theme] .eversolo-login-card .login__brand { display: grid; justify-items: center; diff --git a/src/app/(payload)/admin/_components/eversolo-admin-logo.tsx b/src/app/(payload)/admin/_components/eversolo-admin-logo.tsx new file mode 100644 index 0000000..a17f9b6 --- /dev/null +++ b/src/app/(payload)/admin/_components/eversolo-admin-logo.tsx @@ -0,0 +1,7 @@ +export function EversoloAdminLogo() { + return Eversolo +} + +export function EversoloAdminIcon() { + return E +} diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index cce63b0..640da9b 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -26,6 +26,8 @@ import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { HtmlPreviewTextareaField as HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61 } from '@/payload/admin/HtmlPreviewTextareaField' import { AdminAvatarMenu as AdminAvatarMenu_709e36c272d83f8e640d7dc687c3a445 } from '@/app/(payload)/admin/_components/admin-avatar-menu' +import { EversoloAdminIcon as EversoloAdminIcon_315d959d7586c3df8f4dadece8f5e53b } from '@/app/(payload)/admin/_components/eversolo-admin-logo' +import { EversoloAdminLogo as EversoloAdminLogo_315d959d7586c3df8f4dadece8f5e53b } from '@/app/(payload)/admin/_components/eversolo-admin-logo' import { EversoloPasteURLFetchProvider as EversoloPasteURLFetchProvider_a0494b42366ba3cb335226070f16d4b1 } from '@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' @@ -59,6 +61,8 @@ export const importMap = { "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@/payload/admin/HtmlPreviewTextareaField#HtmlPreviewTextareaField": HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61, "@/app/(payload)/admin/_components/admin-avatar-menu#AdminAvatarMenu": AdminAvatarMenu_709e36c272d83f8e640d7dc687c3a445, + "@/app/(payload)/admin/_components/eversolo-admin-logo#EversoloAdminIcon": EversoloAdminIcon_315d959d7586c3df8f4dadece8f5e53b, + "@/app/(payload)/admin/_components/eversolo-admin-logo#EversoloAdminLogo": EversoloAdminLogo_315d959d7586c3df8f4dadece8f5e53b, "@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider#EversoloPasteURLFetchProvider": EversoloPasteURLFetchProvider_a0494b42366ba3cb335226070f16d4b1, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/src/app/Support/tutorial.html/route.ts b/src/app/Support/tutorial.html/route.ts new file mode 100644 index 0000000..077d1c0 --- /dev/null +++ b/src/app/Support/tutorial.html/route.ts @@ -0,0 +1,9 @@ +import type { NextRequest } from 'next/server' +import { resolveCmsRedirect } from '@/lib/cms-redirect' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +export async function GET(request: NextRequest) { + return resolveCmsRedirect({ request }) +} diff --git a/src/app/Support/tutorial/target/[target]/route.ts b/src/app/Support/tutorial/target/[target]/route.ts new file mode 100644 index 0000000..077d1c0 --- /dev/null +++ b/src/app/Support/tutorial/target/[target]/route.ts @@ -0,0 +1,9 @@ +import type { NextRequest } from 'next/server' +import { resolveCmsRedirect } from '@/lib/cms-redirect' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +export async function GET(request: NextRequest) { + return resolveCmsRedirect({ request }) +} diff --git a/src/lib/cms-redirect.ts b/src/lib/cms-redirect.ts new file mode 100644 index 0000000..701c286 --- /dev/null +++ b/src/lib/cms-redirect.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getPayloadClient } from '@/lib/payload' + +type ResolveCmsRedirectOptions = { + locale?: 'any' | 'en' | 'zh' + pathname?: string + request: NextRequest +} + +function isSafeRedirectTarget(value: string) { + return value.startsWith('/') && !value.startsWith('//') +} + +export async function resolveCmsRedirect({ locale = 'any', pathname, request }: ResolveCmsRedirectOptions) { + const payload = await getPayloadClient() + const rawPathname = pathname ?? new URL(request.url).pathname + + const redirects = await payload.find({ + collection: 'redirects', + depth: 0, + limit: 1, + locale: locale === 'any' ? 'en' : locale, + where: { + and: [ + { enabled: { equals: true } }, + { from: { equals: rawPathname } }, + { + or: [ + { locale: { equals: locale } }, + { locale: { equals: 'any' } }, + { locale: { exists: false } }, + ], + }, + ], + }, + }) + + const redirectEntry = redirects.docs[0] + + if (!redirectEntry?.to || !isSafeRedirectTarget(redirectEntry.to)) { + return NextResponse.json({ error: 'Redirect not found' }, { status: 404 }) + } + + return NextResponse.redirect(new URL(redirectEntry.to, request.url), Number(redirectEntry.statusCode)) +} diff --git a/src/migrations/20260428_011500_seed_support_tutorial_redirects.ts b/src/migrations/20260428_011500_seed_support_tutorial_redirects.ts new file mode 100644 index 0000000..7c5bd85 --- /dev/null +++ b/src/migrations/20260428_011500_seed_support_tutorial_redirects.ts @@ -0,0 +1,52 @@ +import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + INSERT INTO "redirects" ( + "from", + "to", + "status_code", + "match_type", + "locale", + "notes", + "enabled", + "updated_at", + "created_at" + ) + VALUES + ( + '/Support/tutorial.html', + '/en/support/tutorial', + '301', + 'exact', + 'any', + 'Legacy support tutorial landing', + true, + now(), + now() + ), + ( + '/Support/tutorial/target/AjLkU%5Bld%5DqhKb8%3D.html', + '/en/support/tutorial', + '301', + 'exact', + 'any', + 'Legacy support tutorial detail', + true, + now(), + now() + ) + ON CONFLICT DO NOTHING; + `) +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + DELETE FROM "redirects" + WHERE "from" IN ( + '/Support/tutorial.html', + '/Support/tutorial/target/AjLkU%5Bld%5DqhKb8%3D.html' + ) + AND "to" = '/en/support/tutorial'; + `) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 33606a4..894f34f 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -20,6 +20,7 @@ import * as migration_20260427_235500_add_reviews_and_tutorial_page_globals from import * as migration_20260428_001500_remove_video_placeholder_copy from './20260428_001500_remove_video_placeholder_copy'; import * as migration_20260428_002500_add_support_tutorial_videos_collection from './20260428_002500_add_support_tutorial_videos_collection'; import * as migration_20260428_003500_support_tutorial_media_thumbnails from './20260428_003500_support_tutorial_media_thumbnails'; +import * as migration_20260428_011500_seed_support_tutorial_redirects from './20260428_011500_seed_support_tutorial_redirects'; export const migrations = [ { @@ -132,4 +133,9 @@ export const migrations = [ down: migration_20260428_003500_support_tutorial_media_thumbnails.down, name: '20260428_003500_support_tutorial_media_thumbnails' }, + { + up: migration_20260428_011500_seed_support_tutorial_redirects.up, + down: migration_20260428_011500_seed_support_tutorial_redirects.down, + name: '20260428_011500_seed_support_tutorial_redirects' + }, ]; diff --git a/src/payload.config.ts b/src/payload.config.ts index 361d62b..31303a0 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -63,6 +63,16 @@ export default buildConfig({ }, }, components: { + graphics: { + Icon: { + path: '@/app/(payload)/admin/_components/eversolo-admin-logo', + exportName: 'EversoloAdminIcon', + }, + Logo: { + path: '@/app/(payload)/admin/_components/eversolo-admin-logo', + exportName: 'EversoloAdminLogo', + }, + }, providers: [ { path: '@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider', diff --git a/src/scripts/smoke-check.mjs b/src/scripts/smoke-check.mjs index e579935..9562b03 100644 --- a/src/scripts/smoke-check.mjs +++ b/src/scripts/smoke-check.mjs @@ -31,6 +31,8 @@ const routes = [ '/api/health', '/api/admin/captcha', '/favicon.ico', + '/Support/tutorial.html', + '/Support/tutorial/target/AjLkU%5Bld%5DqhKb8%3D.html', '/Product/index/model/DAC-Z10/target/X4C68nRijzjeq7k9e%5Bld%5D3ulg%3D%3D.html', ] @@ -46,6 +48,7 @@ async function check(route) { ok: response.status === 200 || (route === '/admin' && [307, 308].includes(response.status) && Boolean(location?.includes('/admin/login'))) || + (route.startsWith('/Support/tutorial') && [301, 307, 308].includes(response.status) && Boolean(location?.includes('/en/support/tutorial'))) || (route.includes('/Product/index/model/') && response.status === 301 && Boolean(location)) || (route === '/' && response.status === 307 && Boolean(location)), } diff --git a/src/seed/import-seed.mjs b/src/seed/import-seed.mjs index fa17c7b..8589e12 100644 --- a/src/seed/import-seed.mjs +++ b/src/seed/import-seed.mjs @@ -2321,6 +2321,8 @@ async function updateRedirects(payload, report) { { from: '/Support/support.html', to: '/en/support', locale: 'any', notes: 'Legacy support landing' }, { from: '/Support/downloads.html', to: '/en/downloads', locale: 'any', notes: 'Legacy downloads landing' }, { from: '/Support/faq.html', to: '/en/support/faq', locale: 'any', notes: 'Legacy FAQ landing' }, + { from: '/Support/tutorial.html', to: '/en/support/tutorial', locale: 'any', notes: 'Legacy support tutorial landing' }, + { from: '/Support/tutorial/target/AjLkU%5Bld%5DqhKb8%3D.html', to: '/en/support/tutorial', locale: 'any', notes: 'Legacy support tutorial detail' }, { from: '/Contact/afterservice.html', to: '/en/support/contact', locale: 'any', notes: 'Legacy after-service form' }, { from: '/Contact/distributor.html', to: '/en/dealers', locale: 'any', notes: 'Legacy distributor page' }, { from: '/Contact/oem.html', to: '/en/dealers', locale: 'any', notes: 'Legacy OEM page' },