Harden production deployment workflow

This commit is contained in:
Codex
2026-04-28 01:09:17 +08:00
parent c8ccaa9925
commit 46a72f815e
23 changed files with 243 additions and 21 deletions
+1 -1
View File
@@ -164,7 +164,7 @@ async function getContactUsPageData(locale: Locale) {
return payload.findGlobal({
depth: 1,
locale,
slug: 'contactUsPage' as any,
slug: 'contactUsPage',
})
}
@@ -341,7 +341,7 @@ async function getDownloadsPageSettings(locale: Locale): Promise<DownloadsPageSe
return (await payload.findGlobal({
depth: 1,
locale,
slug: 'downloadsPage' as any,
slug: 'downloadsPage',
})) as DownloadsPageSettings
}
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Metadata } from 'next'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages, setRequestLocale } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { isLocale, locales } from '@/i18n/config'
import { isLocale } from '@/i18n/config'
import { CookieConsent } from '@/components/site/cookie-consent'
import { ScrollToTop } from '@/components/site/scroll-to-top'
import { SiteHeader } from '@/components/site/site-header'
@@ -15,7 +15,7 @@ import { ProductContentTabs } from './product-content-tabs'
import { ProductGallerySection } from './product-gallery-section'
import { ProductTemplateMount } from './product-template-mount'
import { ProductVideosSection } from './product-videos-section'
import type { Media, News, NewsCategory, Product, ProductCategory, ProductReview, ProductVideo } from '@/payload-types'
import type { Media, News, NewsCategory, Product, ProductCategory, ProductReview } from '@/payload-types'
export const revalidate = 300
@@ -191,7 +191,7 @@ export default async function ProductsPage({
const payload = await getPayloadClient()
const page = await payload.findGlobal({
depth: 1,
slug: 'productsPage' as any,
slug: 'productsPage',
locale,
})
const categories = await payload.find({
+35
View File
@@ -0,0 +1,35 @@
import { NextResponse } from 'next/server'
import { getPayloadClient } from '@/lib/payload'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export async function GET() {
try {
const payload = await getPayloadClient()
await payload.find({
collection: 'products',
depth: 0,
limit: 1,
pagination: false,
})
return NextResponse.json({
ok: true,
service: 'eversoloweb',
database: 'ok',
timestamp: new Date().toISOString(),
})
} catch (error) {
return NextResponse.json(
{
ok: false,
service: 'eversoloweb',
database: 'error',
error: error instanceof Error ? error.message : 'Unknown health check failure',
timestamp: new Date().toISOString(),
},
{ status: 503 },
)
}
}
@@ -90,7 +90,6 @@ function ProductImage({
return (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={cardImageUrl}
alt={getProductDisplayAlt(product)}
@@ -101,7 +100,6 @@ function ProductImage({
].join(' ')}
/>
{lifestyleImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={lifestyleImageUrl}
alt={getProductDisplayAlt(product)}
+1 -1
View File
@@ -54,7 +54,7 @@ export function HomeHeroBanners({ items, locale }: Props) {
const isExternal = isExternalHref(item.href)
const content = (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
{ }
<img
src={item.imageUrl}
alt={item.alt}
-1
View File
@@ -57,7 +57,6 @@ export function HomeHero({
playsInline
/>
) : heroImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={heroImageUrl}
alt={headline || 'Eversolo'}
-1
View File
@@ -46,7 +46,6 @@ export function HomeNews({ items, locale }: Props) {
className="block overflow-hidden bg-[linear-gradient(135deg,#e7e0d4_0%,#f7f4ee_100%)]"
>
{item.imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={item.imageUrl}
alt={item.title}
-2
View File
@@ -61,7 +61,6 @@ export function HomeRecognition({
const content = (
<div className="flex min-h-[84px] min-w-[220px] items-center gap-4 px-5 py-4 md:min-w-[250px] md:px-6">
{mediaUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={mediaUrl}
alt={award.label || ''}
@@ -111,7 +110,6 @@ export function HomeRecognition({
<div className="flex items-center gap-3">
<div className="h-11 w-11 shrink-0 overflow-hidden rounded-full bg-[linear-gradient(180deg,#f3efe8_0%,#ece7df_100%)]">
{mediaUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={mediaUrl}
alt={sourceName}
+1 -1
View File
@@ -26,7 +26,7 @@ export function HomeStatement({
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(190,169,129,0.18),transparent_32%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.05),transparent_26%)]" />
{imageUrl ? (
<div className="pointer-events-none absolute inset-y-0 right-0 hidden w-[42%] overflow-hidden lg:block">
{/* eslint-disable-next-line @next/next/no-img-element */}
{ }
<img
src={imageUrl}
alt={title || 'Eversolo'}
+11 -2
View File
@@ -44,6 +44,15 @@ const dirname = path.dirname(filename)
const shouldPushSchema =
process.env.PAYLOAD_DB_PUSH === 'true' ||
(process.env.NODE_ENV !== 'production' && process.env.PAYLOAD_DB_PUSH !== 'false')
const shouldRunProdMigrationsOnStart = process.env.PAYLOAD_RUN_MIGRATIONS_ON_START === 'true'
const payloadSecret = process.env.PAYLOAD_SECRET
if (
process.env.NODE_ENV === 'production' &&
(!payloadSecret || payloadSecret === 'change-me' || payloadSecret === 'replace-me-with-a-long-random-string')
) {
throw new Error('PAYLOAD_SECRET must be set to a strong production value before starting Payload.')
}
export default buildConfig({
admin: {
@@ -108,7 +117,7 @@ export default buildConfig({
defaultLocale: 'en',
fallback: false,
},
secret: process.env.PAYLOAD_SECRET ?? 'change-me',
secret: payloadSecret ?? 'development-payload-secret',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
@@ -118,7 +127,7 @@ export default buildConfig({
},
migrationDir: path.resolve(dirname, 'migrations'),
push: shouldPushSchema,
prodMigrations: migrations,
prodMigrations: shouldRunProdMigrationsOnStart ? migrations : undefined,
}),
upload: {
limits: {
@@ -39,6 +39,7 @@ export function HtmlPreviewTextareaField(props: TextareaFieldClientProps) {
aria-selected={mode === 'html'}
className="html-preview-textarea__tab"
onClick={() => setMode('html')}
role="tab"
type="button"
>
HTML
@@ -47,6 +48,7 @@ export function HtmlPreviewTextareaField(props: TextareaFieldClientProps) {
aria-selected={mode === 'preview'}
className="html-preview-textarea__tab"
onClick={() => setMode('preview')}
role="tab"
type="button"
>
Preview
+3 -3
View File
@@ -1,4 +1,4 @@
import type { CollectionConfig, Field } from 'payload'
import type { CollectionConfig, Field, Payload, Where } from 'payload'
import { getProductTemplateOptions } from '@/lib/admin-template-options'
import { compactArrayAdmin } from '@/payload/admin/compact-array-admin'
import { independentDocListFilter } from '@/payload/admin/localized-list-filter'
@@ -31,7 +31,7 @@ const seoFields: Field[] = [
async function preventDuplicateProductIdentity(args: {
data?: Record<string, unknown> | null
originalDoc?: Record<string, unknown> | null
req?: { payload?: any }
req?: { payload?: Payload }
}) {
const { data, originalDoc, req } = args
if (!data || !req?.payload) return data
@@ -40,7 +40,7 @@ async function preventDuplicateProductIdentity(args: {
const model = typeof data.model === 'string' ? data.model.trim() : typeof originalDoc?.model === 'string' ? originalDoc.model.trim() : ''
const slug = typeof data.slug === 'string' ? data.slug.trim() : typeof originalDoc?.slug === 'string' ? originalDoc.slug.trim() : ''
const currentId = originalDoc?.id
const duplicateChecks = []
const duplicateChecks: Where[] = []
if (docLocale && model) {
duplicateChecks.push({ model: { equals: model } })
+12 -1
View File
@@ -18,8 +18,16 @@ const routes = [
'/zh/dealers',
'/en/dealers?tab=stores',
'/zh/dealers?tab=stores',
'/en/reviews',
'/en/reviews?tab=reviews',
'/zh/reviews',
'/en/support',
'/zh/support',
'/en/support/contact',
'/zh/support/contact',
'/en/support/tutorial',
'/zh/support/tutorial',
'/api/health',
'/favicon.ico',
'/Product/index/model/DAC-Z10/target/X4C68nRijzjeq7k9e%5Bld%5D3ulg%3D%3D.html',
]
@@ -40,7 +48,10 @@ async function check(route) {
}
}
const results = await Promise.all(routes.map((route) => check(route)))
const results = []
for (const route of routes) {
results.push(await check(route))
}
for (const result of results) {
const suffix = result.location ? ` -> ${result.location}` : ''