import { getPayload } from "payload"; import configPromise from "../payload.config"; import fs from "fs"; import path from "path"; import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser"; function parseMatter(content: string) { const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!match) return { data: {}, content }; const data: Record = {}; match[1].split("\n").forEach((line) => { const [key, ...rest] = line.split(":"); if (key && rest.length) { const field = key.trim(); let val = rest.join(":").trim(); if (val.startsWith("[")) { // basic array parsing data[field] = val .slice(1, -1) .split(",") .map((s) => s.trim().replace(/^["']|["']$/g, "")); } else { data[field] = val.replace(/^["']|["']$/g, ""); } } }); return { data, content: match[2].trim() }; } async function run() { const payload = await getPayload({ config: configPromise }); const contentDir = path.join(process.cwd(), "content", "blog"); const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx")); for (const file of files) { const filePath = path.join(contentDir, file); const content = fs.readFileSync(filePath, "utf-8"); const { data, content: body } = parseMatter(content); const slug = file.replace(/\.mdx$/, ""); console.log(`Migrating ${slug}...`); try { const existing = await payload.find({ collection: "posts", where: { slug: { equals: slug } }, }); const lexicalBlocks = parseMarkdownToLexical(body); const lexicalAST = { root: { type: "root", format: "", indent: 0, version: 1, children: lexicalBlocks, direction: "ltr", }, }; // Handle thumbnail mapping let featuredImageId = null; if (data.thumbnail) { try { // Remove leading slash and find local file const localPath = path.join( process.cwd(), "public", data.thumbnail.replace(/^\//, ""), ); const fileName = path.basename(localPath); if (fs.existsSync(localPath)) { // Check if media already exists in Payload const existingMedia = await payload.find({ collection: "media", where: { filename: { equals: fileName } }, }); if (existingMedia.docs.length > 0) { featuredImageId = existingMedia.docs[0].id; } else { // Upload new media item const fileData = fs.readFileSync(localPath); const { size } = fs.statSync(localPath); const newMedia = await payload.create({ collection: "media", data: { alt: data.title || fileName, }, file: { data: fileData, name: fileName, mimetype: fileName.endsWith(".png") ? "image/png" : fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") ? "image/jpeg" : "image/webp", size, }, }); featuredImageId = newMedia.id; console.log(` ↑ Uploaded thumbnail: ${fileName}`); } } } catch (e) { console.warn( ` ⚠ Warning: Could not process thumbnail ${data.thumbnail}`, ); } } if (existing.docs.length === 0) { await payload.create({ collection: "posts", data: { title: data.title || slug, slug, description: data.description || "", date: data.date ? new Date(data.date).toISOString() : new Date().toISOString(), tags: (data.tags || []).map((t: string) => ({ tag: t })), content: lexicalAST as any, featuredImage: featuredImageId, }, }); console.log(`✔ Inserted ${slug}`); } else { await payload.update({ collection: "posts", id: existing.docs[0].id, data: { content: lexicalAST as any, featuredImage: featuredImageId, }, }); console.log(`✔ Updated AST and thumbnail for ${slug}`); } } catch (err: any) { console.error(`✘ FAILED ${slug}: ${err.message}`); if (err.data?.errors) { console.error( ` Validation errors:`, JSON.stringify(err.data.errors, null, 2), ); } } } console.log("Migration complete."); process.exit(0); } run().catch(console.error);