Update Payload CMS configuration, collections (Audit, Posts), and add migration scripts/reports.
208 lines
5.6 KiB
TypeScript
208 lines
5.6 KiB
TypeScript
/**
|
|
* Data Transformers
|
|
* Story 1.3: Content Migration Script
|
|
*
|
|
* Transforms Webflow data to Payload CMS format
|
|
*/
|
|
|
|
import type {
|
|
PayloadCategory,
|
|
PayloadPostData,
|
|
PayloadPortfolioData,
|
|
WebflowCategory,
|
|
WebflowPost,
|
|
WebflowPortfolioItem,
|
|
} from './types'
|
|
import { toSlug, splitColorToTextBackground, truncate, htmlToPlainText, parseDate } from './utils'
|
|
import { htmlToLexical } from './lexicalConverter'
|
|
|
|
// ============================================================
|
|
// CATEGORY TRANSFORMER
|
|
// ============================================================
|
|
|
|
/**
|
|
* Transform Webflow category to Payload CMS format
|
|
*/
|
|
export function transformCategory(
|
|
webflowCategory: WebflowCategory,
|
|
order: number = 0,
|
|
): PayloadCategory {
|
|
const { textColor, backgroundColor } = splitColorToTextBackground(
|
|
webflowCategory.colorHex || '#ffffff',
|
|
)
|
|
|
|
return {
|
|
title: webflowCategory.name,
|
|
nameEn: '', // Can be manually set later
|
|
order,
|
|
textColor,
|
|
backgroundColor,
|
|
slug: webflowCategory.slug || toSlug(webflowCategory.name),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform multiple categories
|
|
*/
|
|
export function transformCategories(
|
|
webflowCategories: WebflowCategory[],
|
|
): PayloadCategory[] {
|
|
return webflowCategories.map((cat, index) => transformCategory(cat, index))
|
|
}
|
|
|
|
// ============================================================
|
|
// POST TRANSFORMER
|
|
// ============================================================
|
|
|
|
/**
|
|
* Transform Webflow post to Payload CMS format
|
|
*/
|
|
export function transformPost(webflowPost: WebflowPost): PayloadPostData {
|
|
// Generate excerpt from content if not provided
|
|
const excerpt = webflowPost.excerpt || htmlToPlainText(webflowPost.content, 200)
|
|
|
|
// Convert HTML to Lexical JSON string format (for richText field storage)
|
|
const lexicalContent = htmlToLexical(webflowPost.content || '')
|
|
|
|
return {
|
|
title: webflowPost.title,
|
|
slug: webflowPost.slug || toSlug(webflowPost.title),
|
|
heroImage: undefined, // Will be set by media handler
|
|
ogImage: undefined, // Will be set by media handler
|
|
content: lexicalContent as any, // Lexical JSON string for richText field
|
|
excerpt: truncate(excerpt, 200),
|
|
publishedAt: parseDate(webflowPost.publishedDate),
|
|
status: 'published',
|
|
categories: [], // Will be resolved after categories are migrated
|
|
meta: {
|
|
title: webflowPost.seoTitle || webflowPost.title,
|
|
description: webflowPost.seoDescription || excerpt,
|
|
image: undefined, // Will be set by media handler
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform multiple posts
|
|
*/
|
|
export function transformPosts(webflowPosts: WebflowPost[]): PayloadPostData[] {
|
|
return webflowPosts.map((post) => transformPost(post))
|
|
}
|
|
|
|
// ============================================================
|
|
// PORTFOLIO TRANSFORMER
|
|
// ============================================================
|
|
|
|
/**
|
|
* Transform Webflow portfolio item to Payload CMS format
|
|
*/
|
|
export function transformPortfolio(
|
|
webflowPortfolio: WebflowPortfolioItem,
|
|
): PayloadPortfolioData {
|
|
return {
|
|
title: webflowPortfolio.name,
|
|
slug: webflowPortfolio.slug || toSlug(webflowPortfolio.name),
|
|
url: webflowPortfolio.websiteLink,
|
|
image: undefined, // Will be set by media handler
|
|
description: webflowPortfolio.description,
|
|
websiteType: webflowPortfolio.websiteType || 'other',
|
|
tags: parseTagsString(webflowPortfolio.tags),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform multiple portfolio items
|
|
*/
|
|
export function transformPortfolios(
|
|
webflowPortfolios: WebflowPortfolioItem[],
|
|
): PayloadPortfolioData[] {
|
|
return webflowPortfolios.map((item) => transformPortfolio(item))
|
|
}
|
|
|
|
// ============================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================
|
|
|
|
/**
|
|
* Parse comma-separated tags into array
|
|
*/
|
|
function parseTagsString(tagsString: string): Array<{ tag: string }> {
|
|
if (!tagsString || typeof tagsString !== 'string') {
|
|
return []
|
|
}
|
|
return tagsString
|
|
.split(',')
|
|
.map((tag) => tag.trim())
|
|
.filter(Boolean)
|
|
.map((tag) => ({ tag }))
|
|
}
|
|
|
|
// ============================================================
|
|
// VALIDATION FUNCTIONS
|
|
// ============================================================
|
|
|
|
/**
|
|
* Validate transformed category
|
|
*/
|
|
export function validateCategory(category: PayloadCategory): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = []
|
|
|
|
if (!category.title) {
|
|
errors.push('Category title is required')
|
|
}
|
|
if (!category.slug) {
|
|
errors.push('Category slug is required')
|
|
}
|
|
if (!category.textColor || !category.backgroundColor) {
|
|
errors.push('Category colors are required')
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors }
|
|
}
|
|
|
|
/**
|
|
* Validate transformed post
|
|
*/
|
|
export function validatePost(post: PayloadPostData): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = []
|
|
|
|
if (!post.title) {
|
|
errors.push('Post title is required')
|
|
}
|
|
if (!post.slug) {
|
|
errors.push('Post slug is required')
|
|
}
|
|
if (!post.content) {
|
|
errors.push('Post content is required')
|
|
}
|
|
if (!post.publishedAt) {
|
|
errors.push('Post published date is required')
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors }
|
|
}
|
|
|
|
/**
|
|
* Validate transformed portfolio item
|
|
*/
|
|
export function validatePortfolio(
|
|
portfolio: PayloadPortfolioData,
|
|
): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = []
|
|
|
|
if (!portfolio.title) {
|
|
errors.push('Portfolio title is required')
|
|
}
|
|
if (!portfolio.slug) {
|
|
errors.push('Portfolio slug is required')
|
|
}
|
|
if (!portfolio.url) {
|
|
errors.push('Portfolio URL is required')
|
|
}
|
|
if (!portfolio.websiteType) {
|
|
errors.push('Portfolio website type is required')
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors }
|
|
}
|