/** * 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 } }