Use CMS redirects for support tutorials

This commit is contained in:
Codex
2026-04-28 09:22:18 +08:00
parent 98e32a4385
commit 9a55b874a5
14 changed files with 175 additions and 4 deletions
+25
View File
@@ -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;
@@ -0,0 +1,7 @@
export function EversoloAdminLogo() {
return <span className="eversolo-admin-logo">Eversolo</span>
}
export function EversoloAdminIcon() {
return <span className="eversolo-admin-icon">E</span>
}
+4
View File
@@ -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
}
+9
View File
@@ -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 })
}
@@ -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 })
}
+45
View File
@@ -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))
}
@@ -0,0 +1,52 @@
import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db }: MigrateUpArgs): Promise<void> {
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<void> {
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';
`)
}
+6
View File
@@ -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'
},
];
+10
View File
@@ -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',
+3
View File
@@ -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)),
}
+2
View File
@@ -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' },