feat: payload cms
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s

This commit is contained in:
2026-02-24 02:28:48 +01:00
parent 41cfe19cbf
commit a5d77fc69b
89 changed files with 25282 additions and 1903 deletions

153
scripts/migrate-products.ts Normal file
View File

@@ -0,0 +1,153 @@
import { getPayload } from 'payload';
import configPromise from '../payload.config';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { parseMarkdownToLexical } from '../src/payload/utils/lexicalParser';
async function mapImageToMediaId(payload: any, imagePath: string): Promise<string | null> {
if (!imagePath) return null;
const filename = path.basename(imagePath);
// Exact match instead of substring to avoid matching "cable-black.jpg" with "cable.jpg"
const media = await payload.find({
collection: 'media',
where: {
filename: {
equals: filename,
},
},
limit: 1,
});
if (media.docs.length > 0) {
return media.docs[0].id;
}
const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
const fullPath = path.join(process.cwd(), 'public', cleanPath);
if (fs.existsSync(fullPath)) {
try {
console.log(`[Products Migration] 📤 Ingesting missing Media into Payload: ${filename}`);
const newMedia = await payload.create({
collection: 'media',
data: {
alt: filename.replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, ''),
},
filePath: fullPath,
});
return newMedia.id;
} catch (err: any) {
console.error(`[Products Migration] ❌ Failed to ingest ${filename}:`, err);
}
} else {
console.warn(`[Products Migration] ⚠️ Missing image entirely from disk: ${fullPath}`);
}
return null;
}
export async function migrateProducts() {
const payload = await getPayload({ config: configPromise });
const productLocales = ['en', 'de'];
for (const locale of productLocales) {
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
if (!fs.existsSync(productsDir)) continue;
// Recursive file finder
const mdFiles: string[] = [];
const walk = (dir: string) => {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath);
} else if (file.endsWith('.mdx')) {
mdFiles.push(fullPath);
}
}
};
walk(productsDir);
for (const filePath of mdFiles) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
console.log(`Processing Product: [${locale.toUpperCase()}] ${data.title}`);
// 1. Process Images
const mediaIds = [];
if (data.images && Array.isArray(data.images)) {
for (const imgPath of data.images) {
const id = await mapImageToMediaId(payload, imgPath);
if (id) mediaIds.push(id);
}
}
// 2. Map Lexical AST for deeply nested components (like ProductTabs + Technical data)
const lexicalContent = parseMarkdownToLexical(content);
const wrapLexical = (blocks: any[]) => ({
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: blocks,
direction: 'ltr',
},
});
// Payload expects category objects via the 'category' key
const formattedCategories = Array.isArray(data.categories)
? data.categories.map((c: string) => ({ category: c }))
: [];
const productData = {
title: data.title,
sku: data.sku || path.basename(filePath, '.mdx'),
slug: path.basename(filePath, '.mdx'),
locale: locale as 'en' | 'de',
categories: formattedCategories,
description: data.description || '',
featuredImage: mediaIds.length > 0 ? mediaIds[0] : undefined,
images: mediaIds.length > 0 ? mediaIds : undefined,
content: wrapLexical(lexicalContent) as any,
application: data.application
? (wrapLexical(parseMarkdownToLexical(data.application)) as any)
: undefined,
_status: 'published' as any,
};
// Check if product exists (by sku combined with locale, since slug may differ by language)
const existing = await payload.find({
collection: 'products',
where: {
and: [{ slug: { equals: productData.slug } }, { locale: { equals: locale } }],
},
});
if (existing.docs.length > 0) {
console.log(`Updating existing product ${productData.slug} (${locale})`);
await payload.update({
collection: 'products',
id: existing.docs[0].id,
data: productData,
});
} else {
console.log(`Creating new product ${productData.slug} (${locale})`);
await payload.create({
collection: 'products',
data: productData,
});
}
}
}
console.log(`\n✅ Products Migration Complete!`);
process.exit(0);
}
migrateProducts().catch(console.error);