Use CMS redirects for support tutorials
This commit is contained in:
+2
-2
@@ -85,8 +85,8 @@ Locale is inferred from the user's current site locale, not from the legacy URL
|
|||||||
## Footer / Secondary Links
|
## Footer / Secondary Links
|
||||||
| Legacy | New | Code |
|
| Legacy | New | Code |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/Support/tutorial.html` | `/{locale}/support/tutorial` | 301 |
|
| `/Support/tutorial.html` | `/{locale}/support/tutorial` | 301, managed in CMS Redirects |
|
||||||
| `/Support/tutorial/target/<HASH>.html` | `/{locale}/support/tutorial` | 301 |
|
| `/Support/tutorial/target/<HASH>.html` | `/{locale}/support/tutorial` | 301, managed in CMS Redirects |
|
||||||
| `/Contact/afterservice.html` | `/{locale}/support/warranty` | 301 |
|
| `/Contact/afterservice.html` | `/{locale}/support/warranty` | 301 |
|
||||||
| `/Contact/distributor.html` | `/{locale}/dealers` | 301 |
|
| `/Contact/distributor.html` | `/{locale}/dealers` | 301 |
|
||||||
| `/Contact/oem.html` | `/{locale}/support/contact` | 301 |
|
| `/Contact/oem.html` | `/{locale}/support/contact` | 301 |
|
||||||
|
|||||||
@@ -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.
|
- [ ] `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.
|
- [ ] `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.
|
- [ ] 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.
|
- [ ] No new user-facing fallback copy was introduced without a CMS field.
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ const nextConfig = {
|
|||||||
{ source: '/About/index.html', destination: '/en/about', permanent: true },
|
{ source: '/About/index.html', destination: '/en/about', permanent: true },
|
||||||
|
|
||||||
// Footer / secondary
|
// 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/afterservice.html', destination: '/en/support/warranty', permanent: true },
|
||||||
{ source: '/Contact/distributor.html', destination: '/en/dealers', permanent: true },
|
{ source: '/Contact/distributor.html', destination: '/en/dealers', permanent: true },
|
||||||
{ source: '/Contact/oem.html', destination: '/en/support/contact', permanent: true },
|
{ source: '/Contact/oem.html', destination: '/en/support/contact', permanent: true },
|
||||||
|
|||||||
@@ -1118,6 +1118,31 @@ html[data-theme] .eversolo-login-logo {
|
|||||||
text-transform: uppercase;
|
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 {
|
html[data-theme] .eversolo-login-card .login__brand {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
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>
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
|
|||||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { HtmlPreviewTextareaField as HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61 } from '@/payload/admin/HtmlPreviewTextareaField'
|
import { HtmlPreviewTextareaField as HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61 } from '@/payload/admin/HtmlPreviewTextareaField'
|
||||||
import { AdminAvatarMenu as AdminAvatarMenu_709e36c272d83f8e640d7dc687c3a445 } from '@/app/(payload)/admin/_components/admin-avatar-menu'
|
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 { EversoloPasteURLFetchProvider as EversoloPasteURLFetchProvider_a0494b42366ba3cb335226070f16d4b1 } from '@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider'
|
||||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
@@ -59,6 +61,8 @@ export const importMap = {
|
|||||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@/payload/admin/HtmlPreviewTextareaField#HtmlPreviewTextareaField": HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61,
|
"@/payload/admin/HtmlPreviewTextareaField#HtmlPreviewTextareaField": HtmlPreviewTextareaField_753f6c383200cc3519583a44e0773d61,
|
||||||
"@/app/(payload)/admin/_components/admin-avatar-menu#AdminAvatarMenu": AdminAvatarMenu_709e36c272d83f8e640d7dc687c3a445,
|
"@/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,
|
"@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider#EversoloPasteURLFetchProvider": EversoloPasteURLFetchProvider_a0494b42366ba3cb335226070f16d4b1,
|
||||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -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_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_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_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 = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -132,4 +133,9 @@ export const migrations = [
|
|||||||
down: migration_20260428_003500_support_tutorial_media_thumbnails.down,
|
down: migration_20260428_003500_support_tutorial_media_thumbnails.down,
|
||||||
name: '20260428_003500_support_tutorial_media_thumbnails'
|
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'
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
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: [
|
providers: [
|
||||||
{
|
{
|
||||||
path: '@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider',
|
path: '@/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider',
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const routes = [
|
|||||||
'/api/health',
|
'/api/health',
|
||||||
'/api/admin/captcha',
|
'/api/admin/captcha',
|
||||||
'/favicon.ico',
|
'/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',
|
'/Product/index/model/DAC-Z10/target/X4C68nRijzjeq7k9e%5Bld%5D3ulg%3D%3D.html',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ async function check(route) {
|
|||||||
ok:
|
ok:
|
||||||
response.status === 200 ||
|
response.status === 200 ||
|
||||||
(route === '/admin' && [307, 308].includes(response.status) && Boolean(location?.includes('/admin/login'))) ||
|
(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.includes('/Product/index/model/') && response.status === 301 && Boolean(location)) ||
|
||||||
(route === '/' && response.status === 307 && Boolean(location)),
|
(route === '/' && response.status === 307 && Boolean(location)),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2321,6 +2321,8 @@ async function updateRedirects(payload, report) {
|
|||||||
{ from: '/Support/support.html', to: '/en/support', locale: 'any', notes: 'Legacy support landing' },
|
{ 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/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/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/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/distributor.html', to: '/en/dealers', locale: 'any', notes: 'Legacy distributor page' },
|
||||||
{ from: '/Contact/oem.html', to: '/en/dealers', locale: 'any', notes: 'Legacy OEM page' },
|
{ from: '/Contact/oem.html', to: '/en/dealers', locale: 'any', notes: 'Legacy OEM page' },
|
||||||
|
|||||||
Reference in New Issue
Block a user