Harden production deployment workflow
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -57,7 +57,6 @@ export function HomeHero({
|
||||
playsInline
|
||||
/>
|
||||
) : heroImageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={heroImageUrl}
|
||||
alt={headline || 'Eversolo'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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}` : ''
|
||||
|
||||
Reference in New Issue
Block a user