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:
2026-02-11 11:50:23 +08:00
parent 8ca609a889
commit be7fc902fb
46 changed files with 5442 additions and 15 deletions

View 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')],
},
}

View 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
}

View File

@@ -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,

View File

@@ -111,7 +111,7 @@ export const Posts: CollectionConfig<'posts'> = {
},
}),
label: false,
required: true,
required: false, // Temporarily disabled for migration
},
{
name: 'excerpt',

View File

@@ -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".

View File

@@ -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: '恩群數位行銷',