feat(backend): update collections, config and migration tools
Update Payload CMS configuration, collections (Audit, Posts), and add migration scripts/reports.
This commit is contained in:
178
apps/backend/src/Home/config.ts
Normal file
178
apps/backend/src/Home/config.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import { adminOnly } from '../access/adminOnly'
|
||||
import { auditGlobalChange } from '../collections/Audit/hooks/auditHooks'
|
||||
import { revalidateHome } from './hooks/revalidateHome'
|
||||
|
||||
export const Home: GlobalConfig = {
|
||||
slug: 'home',
|
||||
access: {
|
||||
read: () => true,
|
||||
update: adminOnly,
|
||||
},
|
||||
fields: [
|
||||
// Hero Section
|
||||
{
|
||||
name: 'heroHeadline',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: '創造企業更多發展的可能性\n是我們的使命',
|
||||
admin: {
|
||||
description: '首頁 Hero 主標題(支援換行)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'heroSubheadline',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: "It's our destiny to create possibilities for your business.",
|
||||
admin: {
|
||||
description: '首頁 Hero 副標題(英文)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'heroDesktopVideo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
description: '桌面版 Hero 背景影片',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'heroMobileVideo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
description: '手機版 Hero 背景影片(建議較小檔案以節省流量)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'heroFallbackImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
description: '影片載入失敗時的備用圖片',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'heroLogo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
description: 'Hero 區域顯示的 Logo(可選)',
|
||||
},
|
||||
},
|
||||
|
||||
// Service Features Section
|
||||
{
|
||||
name: 'serviceFeatures',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'icon',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
description: '圖示(支援 SVG 或 Emoji,例如:🎯 或 <svg>...</svg>)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '連結 URL(可選)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
maxRows: 4,
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Portfolio Preview Section
|
||||
{
|
||||
name: 'portfolioSection',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'headline',
|
||||
type: 'text',
|
||||
defaultValue: '精選案例',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'subheadline',
|
||||
type: 'text',
|
||||
defaultValue: '探索我們為客戶打造的優質網站',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'itemsToShow',
|
||||
type: 'number',
|
||||
defaultValue: 3,
|
||||
min: 1,
|
||||
max: 6,
|
||||
admin: {
|
||||
description: '顯示多少個作品項目',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// CTA Section
|
||||
{
|
||||
name: 'ctaSection',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'headline',
|
||||
type: 'text',
|
||||
defaultValue: '準備好開始新的旅程了嗎',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
defaultValue: '讓我們一起打造您的數位成功故事',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'buttonText',
|
||||
type: 'text',
|
||||
defaultValue: '聯絡我們',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'buttonLink',
|
||||
type: 'text',
|
||||
defaultValue: '/contact-us',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [revalidateHome, auditGlobalChange('home')],
|
||||
},
|
||||
}
|
||||
14
apps/backend/src/Home/hooks/revalidateHome.ts
Normal file
14
apps/backend/src/Home/hooks/revalidateHome.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { GlobalAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidateHome: GlobalAfterChangeHook = async ({ doc, req }) => {
|
||||
const { payload, context } = req
|
||||
if (!context.disableRevalidate) {
|
||||
payload.logger.info(`Revalidating home`)
|
||||
|
||||
revalidateTag('global_home')
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -8,13 +8,16 @@ import { logDocumentChange } from '@/utilities/auditLogger'
|
||||
*/
|
||||
export const auditChange =
|
||||
(collection: string): AfterChangeHook =>
|
||||
async ({ doc, req }) => {
|
||||
async ({ doc, req, context }) => {
|
||||
// 跳過 audit 集合本身以避免無限循環
|
||||
if (collection === 'audit') return doc
|
||||
|
||||
// Determine operation from context or default to 'update'
|
||||
const operation = (context?.operation as 'create' | 'update' | 'delete') || 'update'
|
||||
|
||||
await logDocumentChange(
|
||||
req,
|
||||
operation as 'create' | 'update' | 'delete',
|
||||
operation,
|
||||
collection,
|
||||
doc.id as string,
|
||||
(doc.title || doc.name || String(doc.id)) as string,
|
||||
|
||||
@@ -111,7 +111,7 @@ export const Posts: CollectionConfig<'posts'> = {
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
required: true,
|
||||
required: false, // Temporarily disabled for migration
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
|
||||
@@ -103,10 +103,12 @@ export interface Config {
|
||||
globals: {
|
||||
header: Header;
|
||||
footer: Footer;
|
||||
home: Home;
|
||||
};
|
||||
globalsSelect: {
|
||||
header: HeaderSelect<false> | HeaderSelect<true>;
|
||||
footer: FooterSelect<false> | FooterSelect<true>;
|
||||
home: HomeSelect<false> | HomeSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
@@ -220,7 +222,7 @@ export interface Post {
|
||||
* Facebook/LINE 分享時顯示的預覽圖,建議 1200x630px
|
||||
*/
|
||||
ogImage?: (string | null) | Media;
|
||||
content: {
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
@@ -234,7 +236,7 @@ export interface Post {
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
} | null;
|
||||
/**
|
||||
* 顯示在文章列表頁,建議 150-200 字
|
||||
*/
|
||||
@@ -1410,6 +1412,70 @@ export interface Footer {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "home".
|
||||
*/
|
||||
export interface Home {
|
||||
id: string;
|
||||
/**
|
||||
* 首頁 Hero 主標題(支援換行)
|
||||
*/
|
||||
heroHeadline: string;
|
||||
/**
|
||||
* 首頁 Hero 副標題(英文)
|
||||
*/
|
||||
heroSubheadline: string;
|
||||
/**
|
||||
* 桌面版 Hero 背景影片
|
||||
*/
|
||||
heroDesktopVideo?: (string | null) | Media;
|
||||
/**
|
||||
* 手機版 Hero 背景影片(建議較小檔案以節省流量)
|
||||
*/
|
||||
heroMobileVideo?: (string | null) | Media;
|
||||
/**
|
||||
* 影片載入失敗時的備用圖片
|
||||
*/
|
||||
heroFallbackImage?: (string | null) | Media;
|
||||
/**
|
||||
* Hero 區域顯示的 Logo(可選)
|
||||
*/
|
||||
heroLogo?: (string | null) | Media;
|
||||
serviceFeatures?:
|
||||
| {
|
||||
/**
|
||||
* 圖示(支援 SVG 或 Emoji,例如:🎯 或 <svg>...</svg>)
|
||||
*/
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
link?: {
|
||||
/**
|
||||
* 連結 URL(可選)
|
||||
*/
|
||||
url?: string | null;
|
||||
};
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
portfolioSection: {
|
||||
headline: string;
|
||||
subheadline?: string | null;
|
||||
/**
|
||||
* 顯示多少個作品項目
|
||||
*/
|
||||
itemsToShow?: number | null;
|
||||
};
|
||||
ctaSection: {
|
||||
headline: string;
|
||||
description?: string | null;
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "header_select".
|
||||
@@ -1470,6 +1536,49 @@ export interface FooterSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "home_select".
|
||||
*/
|
||||
export interface HomeSelect<T extends boolean = true> {
|
||||
heroHeadline?: T;
|
||||
heroSubheadline?: T;
|
||||
heroDesktopVideo?: T;
|
||||
heroMobileVideo?: T;
|
||||
heroFallbackImage?: T;
|
||||
heroLogo?: T;
|
||||
serviceFeatures?:
|
||||
| T
|
||||
| {
|
||||
icon?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
link?:
|
||||
| T
|
||||
| {
|
||||
url?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
portfolioSection?:
|
||||
| T
|
||||
| {
|
||||
headline?: T;
|
||||
subheadline?: T;
|
||||
itemsToShow?: T;
|
||||
};
|
||||
ctaSection?:
|
||||
| T
|
||||
| {
|
||||
headline?: T;
|
||||
description?: T;
|
||||
buttonText?: T;
|
||||
buttonLink?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskCleanup-audit-logs".
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Portfolio } from './collections/Portfolio'
|
||||
import { Posts } from './collections/Posts'
|
||||
import { Users } from './collections/Users'
|
||||
import { Footer } from './Footer/config'
|
||||
import { Home } from './Home/config'
|
||||
import { Header } from './Header/config'
|
||||
import { plugins } from './plugins'
|
||||
import { defaultLexical } from '@/fields/defaultLexical'
|
||||
@@ -71,7 +72,7 @@ export default buildConfig({
|
||||
'http://localhost:4321', // Astro dev server
|
||||
'http://localhost:8788', // Wrangler Pages dev server
|
||||
].filter(Boolean),
|
||||
globals: [Header, Footer],
|
||||
globals: [Header, Footer, Home],
|
||||
email: resendAdapter({
|
||||
defaultFromAddress: 'dev@resend.com',
|
||||
defaultFromName: '恩群數位行銷',
|
||||
|
||||
Reference in New Issue
Block a user