feat: migrate Payload CMS to MDX and harden static infrastructure

This commit is contained in:
2026-05-04 14:48:04 +02:00
parent d51e220802
commit d3141187ee
165 changed files with 7654 additions and 16136 deletions

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env bash
# ────────────────────────────────────────────────────────────────────────────
# Payload CMS Database Backup
# Creates a timestamped pg_dump of the Payload Postgres database.
# Usage: npm run backup:db
# ────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# Load environment variables
if [ -f .env ]; then
set -a; source .env; set +a
fi
DB_NAME="${PAYLOAD_DB_NAME:-payload}"
DB_USER="${PAYLOAD_DB_USER:-payload}"
BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Check if database container is running
if ! docker compose ps --services --filter "status=running" | grep -qx "klz-db"; then
echo "⚠️ Database container 'klz-db' is not running. Starting it..."
docker compose up -d klz-db
echo "⏳ Waiting for database to be ready..."
sleep 3
fi
echo "📦 Backing up Payload database..."
echo " Service: klz-db"
echo " Database: $DB_NAME"
echo " Output: $BACKUP_FILE"
# Run pg_dump inside the container and compress
docker compose exec -T klz-db pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists | gzip > "$BACKUP_FILE"
# Show result
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo ""
echo "✅ Backup complete: $BACKUP_FILE ($SIZE)"
echo ""
# Show existing backups
echo "📋 Available backups:"
ls -lh "$BACKUP_DIR"/*.sql.gz 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'

View File

@@ -1,14 +0,0 @@
import { getPayload } from 'payload';
import configPromise from '@payload-config';
async function run() {
const payload = await getPayload({ config: configPromise });
const result = await payload.find({ collection: 'pages', limit: 1 });
const doc = result.docs[0] as any;
console.log('Sample page:', doc.slug);
console.log('Content structure (first 2 levels):');
console.log(JSON.stringify(doc.content, null, 2).slice(0, 3000));
process.exit(0);
}
run();

View File

@@ -1,14 +0,0 @@
import { getPayload } from 'payload';
import configPromise from '@payload-config';
async function run() {
const payload = await getPayload({ config: configPromise });
const existing = await payload.find({
collection: 'pages',
where: { slug: { equals: 'start' }, locale: { equals: 'de' } },
limit: 1,
});
console.log(JSON.stringify(existing.docs[0].content, null, 2).slice(0, 1500));
process.exit(0);
}
run();

View File

@@ -1,14 +0,0 @@
import { getPayload } from 'payload';
import configPromise from '@payload-config';
async function run() {
const payload = await getPayload({ config: configPromise });
const existing = await payload.find({
collection: 'pages',
where: { slug: { equals: 'team' }, locale: { equals: 'de' } },
limit: 1,
});
console.log(JSON.stringify(existing.docs[0].content, null, 2).slice(0, 500));
process.exit(0);
}
run();

View File

@@ -1,315 +0,0 @@
#!/usr/bin/env bash
# ────────────────────────────────────────────────────────────────────────────
# CMS Data Sync Tool
# Safely syncs Payload CMS data (DB + media) between environments.
#
# Usage:
# cms:push:testing Push local → testing
# cms:push:prod Push local → production
# cms:pull:testing Pull testing → local
# cms:pull:prod Pull production → local
# ────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SYNC_SUCCESS="false"
LOCAL_BACKUP_FILE=""
REMOTE_BACKUP_FILE=""
cleanup_on_exit() {
local exit_code=$?
if [ "$SYNC_SUCCESS" != "true" ] && [ $exit_code -ne 0 ]; then
echo ""
echo "❌ Sync aborted or failed! (Exit code: $exit_code)"
if [ "${DIRECTION:-}" = "push" ] && [ -n "${REMOTE_BACKUP_FILE:-}" ]; then
echo "🔄 Rolling back $TARGET database..."
ssh "$SSH_HOST" "gunzip -c $REMOTE_BACKUP_FILE | docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --quiet" || echo "⚠️ Rollback failed"
echo "✅ Rollback complete."
elif [ "${DIRECTION:-}" = "pull" ] && [ -n "${LOCAL_BACKUP_FILE:-}" ]; then
echo "🔄 Rolling back local database..."
gunzip -c "$LOCAL_BACKUP_FILE" | docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --quiet || echo "⚠️ Rollback failed"
echo "✅ Rollback complete."
fi
fi
}
trap 'cleanup_on_exit' EXIT
# Load environment variables
if [ -f .env ]; then
set -a; source .env; set +a
fi
# ── Configuration ──────────────────────────────────────────────────────────
DIRECTION="${1:-}" # push | pull
TARGET="${2:-}" # testing | prod
SSH_HOST="root@alpha.mintel.me"
LOCAL_DB_USER="${PAYLOAD_DB_USER:-payload}"
LOCAL_DB_NAME="${PAYLOAD_DB_NAME:-payload}"
LOCAL_DB_CONTAINER="klz-2026-klz-db-1"
LOCAL_MEDIA_DIR="./public/media"
BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# Remote credentials (resolved per-target from server env files)
REMOTE_DB_USER=""
REMOTE_DB_NAME=""
# Auto-detect migrations from src/migrations/*.ts (no manual maintenance needed)
MIGRATIONS=()
BATCH=1
for migration_file in $(ls src/migrations/*.ts 2>/dev/null | sort); do
name=$(basename "$migration_file" .ts)
MIGRATIONS+=("$name:$BATCH")
((BATCH++))
done
if [ ${#MIGRATIONS[@]} -eq 0 ]; then
echo "⚠️ No migration files found in src/migrations/"
fi
# ── Resolve target environment ─────────────────────────────────────────────
resolve_target() {
case "$TARGET" in
testing)
REMOTE_PROJECT="klz-testing"
REMOTE_DB_CONTAINER="klz-testing-klz-db-1"
REMOTE_APP_CONTAINER="klz-testing-klz-app-1"
REMOTE_MEDIA_VOLUME="/var/lib/docker/volumes/klz-testing_klz_media_data/_data"
REMOTE_SITE_DIR="/home/deploy/sites/testing.klz-cables.com"
;;
staging)
REMOTE_PROJECT="klz-staging"
REMOTE_DB_CONTAINER="klz-staging-klz-db-1"
REMOTE_APP_CONTAINER="klz-staging-klz-app-1"
REMOTE_MEDIA_VOLUME="/var/lib/docker/volumes/klz-staging_klz_media_data/_data"
REMOTE_SITE_DIR="/home/deploy/sites/staging.klz-cables.com"
;;
prod|production)
REMOTE_PROJECT="klz-cablescom"
REMOTE_DB_CONTAINER="klz-cablescom-klz-db-1"
REMOTE_APP_CONTAINER="klz-cablescom-klz-app-1"
REMOTE_MEDIA_VOLUME="/var/lib/docker/volumes/klz-cablescom_klz_media_data/_data"
REMOTE_SITE_DIR="/home/deploy/sites/klz-cables.com"
;;
*)
echo "❌ Unknown target: $TARGET"
echo " Valid targets: testing, staging, prod"
exit 1
;;
esac
# Auto-detect remote DB credentials from the env file on the server
echo "🔍 Detecting $TARGET database credentials..."
REMOTE_DB_USER=$(ssh "$SSH_HOST" "grep -h '^PAYLOAD_DB_USER=' $REMOTE_SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "")
REMOTE_DB_NAME=$(ssh "$SSH_HOST" "grep -h '^PAYLOAD_DB_NAME=' $REMOTE_SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "")
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
echo " User: $REMOTE_DB_USER | DB: $REMOTE_DB_NAME"
}
# ── Ensure local DB is running ─────────────────────────────────────────────
ensure_local_db() {
if ! docker ps --format '{{.Names}}' | grep -q "$LOCAL_DB_CONTAINER"; then
echo "⏳ Local DB container not running. Starting..."
docker compose up -d klz-db
echo "⏳ Waiting for local DB to be ready..."
for i in $(seq 1 10); do
if docker exec "$LOCAL_DB_CONTAINER" pg_isready -U "$LOCAL_DB_USER" -q 2>/dev/null; then
echo "✅ Local DB is ready."
return
fi
sleep 1
done
echo "❌ Local DB failed to start."
exit 1
fi
}
# ── Sanitize migrations table ──────────────────────────────────────────────
sanitize_migrations() {
local container="$1"
local db_user="$2"
local db_name="$3"
local is_remote="$4" # "true" or "false"
echo "🔧 Sanitizing payload_migrations table..."
local SQL="DELETE FROM payload_migrations WHERE batch = -1;"
for entry in "${MIGRATIONS[@]}"; do
local name="${entry%%:*}"
local batch="${entry##*:}"
SQL="$SQL INSERT INTO payload_migrations (name, batch) SELECT '$name', $batch WHERE NOT EXISTS (SELECT 1 FROM payload_migrations WHERE name = '$name');"
done
if [ "$is_remote" = "true" ]; then
ssh "$SSH_HOST" "docker exec $container psql -U $db_user -d $db_name -c \"$SQL\""
else
docker exec "$container" psql -U "$db_user" -d "$db_name" -c "$SQL"
fi
}
# ── Safety: Create backup before overwriting ───────────────────────────────
backup_local_db() {
mkdir -p "$BACKUP_DIR"
local file="$BACKUP_DIR/payload_pre_sync_${TIMESTAMP}.sql.gz"
echo "📦 Creating safety backup of local DB → $file"
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --clean --if-exists | gzip > "$file"
echo "✅ Backup: $file ($(du -h "$file" | cut -f1))"
LOCAL_BACKUP_FILE="$file"
}
backup_remote_db() {
local file="/tmp/payload_pre_sync_${TIMESTAMP}.sql.gz"
echo "📦 Creating safety backup of $TARGET DB → $SSH_HOST:$file"
ssh "$SSH_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --clean --if-exists | gzip > $file"
echo "✅ Remote backup: $file"
REMOTE_BACKUP_FILE="$file"
}
# ── Pre-flight: Verify remote containers exist ─────────────────────────────
check_remote_containers() {
echo "🔍 Checking $TARGET containers..."
local missing=0
if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_DB_CONTAINER" | grep -q .; then
echo "❌ Database container '$REMOTE_DB_CONTAINER' not found on $SSH_HOST"
echo " → Deploy $TARGET first: git push to trigger pipeline, or run:"
echo " ssh $SSH_HOST \"cd $REMOTE_SITE_DIR && docker compose -p $REMOTE_PROJECT --env-file .env.\$TARGET up -d\""
missing=1
fi
if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_APP_CONTAINER" | grep -q .; then
echo "❌ App container '$REMOTE_APP_CONTAINER' not found on $SSH_HOST"
missing=1
fi
if [ $missing -eq 1 ]; then
echo ""
echo "💡 The $TARGET environment hasn't been deployed yet."
echo " Push to the '$TARGET' branch or run the pipeline first."
exit 1
fi
echo "✅ All $TARGET containers running."
}
# ── PUSH: local → remote ──────────────────────────────────────────────────
do_push() {
echo ""
echo "┌──────────────────────────────────────────────────┐"
echo "│ 📤 PUSH: local → $TARGET "
echo "│ This will OVERWRITE the $TARGET database! "
echo "│ A safety backup will be created first. "
echo "└──────────────────────────────────────────────────┘"
echo ""
read -p "Are you sure? (y/N) " -n 1 -r
echo ""
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
# 0. Ensure local DB is running & remote containers exist
ensure_local_db
check_remote_containers
# 1. Safety backup of remote
backup_remote_db
# 2. Dump local DB
echo "📤 Dumping local database..."
local dump="/tmp/payload_push_${TIMESTAMP}.sql.gz"
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --clean --if-exists | gzip > "$dump"
# 3. Transfer and restore
echo "📤 Transferring to $SSH_HOST..."
scp "$dump" "$SSH_HOST:/tmp/payload_push.sql.gz"
echo "🔄 Restoring database on $TARGET..."
ssh "$SSH_HOST" "gunzip -c /tmp/payload_push.sql.gz | docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --quiet"
# 4. Sanitize migrations
sanitize_migrations "$REMOTE_DB_CONTAINER" "$REMOTE_DB_USER" "$REMOTE_DB_NAME" "true"
# 5. Sync media
echo "🖼️ Syncing media files..."
rsync -az --delete --progress "$LOCAL_MEDIA_DIR/" "$SSH_HOST:$REMOTE_MEDIA_VOLUME/"
# Fix ownership: rsync preserves local UID, but container runs as nextjs (1001)
echo "🔑 Fixing media file permissions..."
ssh "$SSH_HOST" "docker exec -u 0 $REMOTE_APP_CONTAINER chown -R 1001:65533 /app/public/media/ 2>/dev/null || true"
# 6. Restart app
echo "🔄 Restarting $TARGET app container..."
ssh "$SSH_HOST" "docker restart $REMOTE_APP_CONTAINER"
# Cleanup
rm -f "$dump"
ssh "$SSH_HOST" "rm -f /tmp/payload_push.sql.gz"
SYNC_SUCCESS="true"
echo ""
echo "✅ Push to $TARGET complete!"
}
# ── PULL: remote → local ──────────────────────────────────────────────────
do_pull() {
echo ""
echo "┌──────────────────────────────────────────────────┐"
echo "│ 📥 PULL: $TARGET → local "
echo "│ This will OVERWRITE your local database! "
echo "│ A safety backup will be created first. "
echo "└──────────────────────────────────────────────────┘"
echo ""
read -p "Are you sure? (y/N) " -n 1 -r
echo ""
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
# 0. Ensure local DB is running & remote containers exist
ensure_local_db
check_remote_containers
# 1. Safety backup of local
backup_local_db
# 2. Dump remote DB
echo "📥 Dumping $TARGET database..."
ssh "$SSH_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --clean --if-exists | gzip > /tmp/payload_pull.sql.gz"
# 3. Transfer and restore
echo "📥 Downloading from $SSH_HOST..."
scp "$SSH_HOST:/tmp/payload_pull.sql.gz" "/tmp/payload_pull.sql.gz"
echo "🔄 Restoring database locally..."
gunzip -c "/tmp/payload_pull.sql.gz" | docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --quiet
# 4. Sync media
echo "🖼️ Syncing media files..."
mkdir -p "$LOCAL_MEDIA_DIR"
rsync -az --delete --progress "$SSH_HOST:$REMOTE_MEDIA_VOLUME/" "$LOCAL_MEDIA_DIR/"
# Cleanup
rm -f "/tmp/payload_pull.sql.gz"
ssh "$SSH_HOST" "rm -f /tmp/payload_pull.sql.gz"
SYNC_SUCCESS="true"
echo ""
echo "✅ Pull from $TARGET complete! Restart dev server to see changes."
}
# ── Main ───────────────────────────────────────────────────────────────────
if [ -z "$DIRECTION" ] || [ -z "$TARGET" ]; then
echo "📦 CMS Data Sync Tool"
echo ""
echo "Usage:"
echo " pnpm cms:push:testing Push local DB + media → testing"
echo " pnpm cms:push:staging Push local DB + media → staging"
echo " pnpm cms:push:prod Push local DB + media → production"
echo " pnpm cms:pull:testing Pull testing DB + media → local"
echo " pnpm cms:pull:staging Pull staging DB + media → local"
echo " pnpm cms:pull:prod Pull production DB + media → local"
echo ""
echo "Safety: A backup is always created before overwriting."
exit 1
fi
resolve_target
case "$DIRECTION" in
push) do_push ;;
pull) do_pull ;;
*)
echo "❌ Unknown direction: $DIRECTION (use 'push' or 'pull')"
exit 1
;;
esac

View File

@@ -0,0 +1,125 @@
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
function lexicalToMarkdown(node: any, depth = 0): string {
if (!node) return '';
if (typeof node === 'string') return node;
if (node.type === 'text') {
let text = node.text || '';
if (node.format === 1) text = `**${text}**`; // Bold
if (node.format === 2) text = `*${text}*`; // Italic
return text;
}
if (node.type === 'heading') {
const level = parseInt((node.tag || 'h1').replace('h', '')) || 1;
const prefix = '#'.repeat(level);
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
return `${prefix} ${text}\n\n`;
}
if (node.type === 'paragraph') {
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
return text.trim() ? `${text}\n\n` : '\n';
}
if (node.type === 'list') {
const isOrdered = node.tag === 'ol';
const items = (node.children || [])
.map((c: any, index: number) => {
const prefix = isOrdered ? `${index + 1}.` : '-';
return `${prefix} ${lexicalToMarkdown(c, depth + 1).trim()}`;
})
.join('\n');
return `${items}\n\n`;
}
if (node.type === 'listitem') {
return (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
}
if (node.type === 'quote') {
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
return `> ${text}\n\n`;
}
if (node.type === 'link') {
const url = node.fields?.url || node.url || '';
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
return `[${text}](${url})`;
}
if (node.type === 'upload') {
const url = node.value?.url || '';
const alt = node.value?.alt || node.value?.filename || '';
return `![${alt}](${url})\n\n`;
}
if (node.type === 'block') {
const blockType = node.fields?.blockType;
if (blockType === 'contactSection') {
return `<ContactSection showMap={${!!node.fields?.showMap}} showForm={${!!node.fields?.showForm}} showHours={${!!node.fields?.showHours}} />\n\n`;
}
if (blockType === 'heroSection') {
return `<HeroSection badge="${node.fields?.badge || ''}" title="${node.fields?.title || ''}" subtitle="${node.fields?.subtitle || ''}" alignment="${node.fields?.alignment || 'left'}" />\n\n`;
}
if (blockType === 'features') {
return `<FeaturesSection layout="${node.fields?.layout || ''}" />\n\n`;
}
// Generic MDX block wrapper if unknown
return `<Block type="${blockType}" data={${JSON.stringify(node.fields)}} />\n\n`;
}
if (node.children && Array.isArray(node.children)) {
return node.children.map((c: any) => lexicalToMarkdown(c, depth)).join('');
}
return '';
}
async function convertDir(dir: string) {
try {
const files = await fs.readdir(dir);
for (const f of files) {
if (!f.endsWith('.mdx')) continue;
const filePath = path.join(dir, f);
const fileContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(fileContent);
let ast = null;
try {
if (content.trim().startsWith('{')) {
ast = JSON.parse(content);
}
} catch (e) {
// Not JSON
}
if (ast && ast.root) {
const mdContent = lexicalToMarkdown(ast.root);
const newFileContent = matter.stringify(mdContent, data);
await fs.writeFile(filePath, newFileContent);
console.log(`Converted ${filePath}`);
}
}
} catch (error) {
console.error(`Failed reading directory ${dir}`, error);
}
}
async function main() {
const contentDir = path.join(process.cwd(), 'content');
const dirs = await fs.readdir(contentDir);
for (const dir of dirs) {
const fullPath = path.join(contentDir, dir);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await convertDir(fullPath);
}
}
}
main().catch(console.error);

26
scripts/fix-media-urls.ts Normal file
View File

@@ -0,0 +1,26 @@
import * as fs from 'fs';
import * as path from 'path';
function walk(dir: string, callback: (filePath: string) => void) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
walk(filePath, callback);
} else if (filePath.endsWith('.mdx') || filePath.endsWith('.md')) {
callback(filePath);
}
}
}
let replacedCount = 0;
walk(path.join(process.cwd(), 'content'), (filePath) => {
let content = fs.readFileSync(filePath, 'utf8');
if (content.includes('/api/media/file/')) {
content = content.replace(/\/api\/media\/file\//g, '/media/');
fs.writeFileSync(filePath, content, 'utf8');
replacedCount++;
}
});
console.log(`Successfully fixed media URLs in ${replacedCount} files.`);

View File

@@ -1,329 +0,0 @@
#!/usr/bin/env ts-node
/**
* PDF Datasheet Generator (React-PDF)
*
* Renders PDFs via `@react-pdf/renderer`.
*
* Source of truth:
* - All technical data + cross-section tables: Excel files in `data/excel/`
* - Product description text: Fetched dynamically from Payload CMS
*/
import * as fs from 'fs';
import * as path from 'path';
import * as XLSX from 'xlsx';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import type { ProductData } from './pdf/model/types';
import { generateDatasheetPdfBuffer } from './pdf/react-pdf/generate-datasheet-pdf';
import { generateFileName, normalizeValue, stripHtml } from './pdf/model/utils';
const CONFIG = {
outputDir: path.join(process.cwd(), 'public/datasheets'),
chunkSize: 10,
} as const;
const EXCEL_FILES = [
{ path: path.join(process.cwd(), 'data/excel/high-voltage.xlsx'), voltageType: 'high-voltage' },
{
path: path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'),
voltageType: 'medium-voltage',
},
{
path: path.join(process.cwd(), 'data/excel/medium-voltage-KM 170126.xlsx'),
voltageType: 'medium-voltage',
},
{ path: path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'), voltageType: 'low-voltage' },
{ path: path.join(process.cwd(), 'data/excel/solar-cables.xlsx'), voltageType: 'solar' },
] as const;
type CmsProduct = {
slug: string;
title: string;
sku: string;
categories: string[];
images: string[];
descriptionHtml: string;
applicationHtml: string;
};
type CmsIndex = Map<string, CmsProduct>; // key: normalized designation/title
function ensureOutputDir(): void {
if (!fs.existsSync(CONFIG.outputDir)) {
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
}
}
function normalizeExcelKey(value: string): string {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
async function buildCmsIndex(locale: 'en' | 'de'): Promise<CmsIndex> {
const idx: CmsIndex = new Map();
try {
const payload = await getPayload({ config: configPromise });
const isDev = process.env.NODE_ENV === 'development';
const result = await payload.find({
collection: 'products',
where: {
...(!isDev ? { _status: { equals: 'published' } } : {}),
},
locale: locale as any,
pagination: false,
});
for (const doc of result.docs) {
if (!doc.title) continue;
const title = normalizeValue(String(doc.title));
const sku = normalizeValue(String(doc.sku || ''));
const categories = Array.isArray(doc.categories)
? doc.categories.map((c: any) => normalizeValue(String(c.category || c))).filter(Boolean)
: [];
const images = Array.isArray(doc.images)
? doc.images
.map((i: any) => normalizeValue(String(typeof i === 'string' ? i : i.url)))
.filter(Boolean)
: [];
const descriptionHtml = normalizeValue(String(doc.description || ''));
const applicationHtml = ''; // Application usually part of description in Payload now
const slug = doc.slug || '';
idx.set(normalizeExcelKey(title), {
slug,
title,
sku,
categories,
images,
descriptionHtml,
applicationHtml,
});
}
} catch (error) {
console.error(`[Payload] Failed to fetch products for CMS index (${locale}):`, error);
}
return idx;
}
function findKeyByHeaderValue(headerRow: Record<string, unknown>, pattern: RegExp): string | null {
for (const [k, v] of Object.entries(headerRow || {})) {
const text = normalizeValue(String(v ?? ''));
if (!text) continue;
if (pattern.test(text)) return k;
}
return null;
}
function readExcelRows(filePath: string): Array<Record<string, unknown>> {
if (!fs.existsSync(filePath)) return [];
const workbook = XLSX.readFile(filePath, { cellDates: false, cellNF: false, cellText: false });
const sheetName = workbook.SheetNames[0];
if (!sheetName) return [];
const sheet = workbook.Sheets[sheetName];
if (!sheet) return [];
return XLSX.utils.sheet_to_json(sheet, {
defval: '',
raw: false,
blankrows: false,
}) as Array<Record<string, unknown>>;
}
function readDesignationsFromExcelFile(filePath: string): Map<string, string> {
const rows = readExcelRows(filePath);
if (!rows.length) return new Map();
// Legacy sheets use "Part Number" as a column key.
// The new MV sheet uses __EMPTY* keys and stores the human headers in row 0 values.
const headerRow = rows[0] || {};
const partNumberKey =
(Object.prototype.hasOwnProperty.call(headerRow, 'Part Number') ? 'Part Number' : null) ||
findKeyByHeaderValue(headerRow, /^part\s*number$/i) ||
'__EMPTY';
const out = new Map<string, string>();
for (const r of rows) {
const pn = normalizeValue(String(r?.[partNumberKey] ?? ''));
if (!pn || pn === 'Units' || pn === 'Part Number') continue;
const key = normalizeExcelKey(pn);
if (!key) continue;
// Keep first-seen designation string (stable filenames from MDX slug).
if (!out.has(key)) out.set(key, pn);
}
return out;
}
function loadAllExcelDesignations(): Map<string, { designation: string; voltageType: string }> {
const out = new Map<string, { designation: string; voltageType: string }>();
for (const file of EXCEL_FILES) {
const m = readDesignationsFromExcelFile(file.path);
Array.from(m.entries()).forEach(([k, v]) => {
if (!out.has(k)) out.set(k, { designation: v, voltageType: file.voltageType });
});
}
return out;
}
async function loadProductsFromExcelAndCms(locale: 'en' | 'de'): Promise<ProductData[]> {
const cmsIndex = await buildCmsIndex(locale);
const excelDesignations = loadAllExcelDesignations();
const products: ProductData[] = [];
let id = 1;
Array.from(excelDesignations.entries()).forEach(([key, data]) => {
const cmsItem = cmsIndex.get(key) || null;
const title = cmsItem?.title || data.designation;
const slug =
cmsItem?.slug ||
title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
// Only the product description comes from CMS. Everything else is Excel-driven
// during model building (technicalItems + voltage tables).
const descriptionHtml = cmsItem?.descriptionHtml || '';
products.push({
id: id++,
name: title,
shortDescriptionHtml: '',
descriptionHtml,
applicationHtml: cmsItem?.applicationHtml || '',
images: cmsItem?.images || [],
featuredImage: (cmsItem?.images && cmsItem.images[0]) || null,
sku: cmsItem?.sku || title,
slug,
translationKey: slug,
locale,
categories: (cmsItem?.categories || []).map((name) => ({ name })),
attributes: [],
voltageType: (() => {
const cats = (cmsItem?.categories || []).map((c) => String(c));
const isMV = cats.some((c) => /medium[-\s]?voltage|mittelspannung/i.test(c));
if (isMV && data.voltageType === 'high-voltage') return 'medium-voltage';
return data.voltageType;
})(),
});
});
// Deterministic order: by slug, then name.
products.sort(
(a, b) => (a.slug || '').localeCompare(b.slug || '') || a.name.localeCompare(b.name),
);
// Drop products that have no readable name.
return products.filter((p) => stripHtml(p.name));
}
async function processChunk(
products: ProductData[],
chunkIndex: number,
totalChunks: number,
): Promise<void> {
console.log(
`\nProcessing chunk ${chunkIndex + 1}/${totalChunks} (${products.length} products)...`,
);
for (const product of products) {
try {
const locale = (product.locale || 'en') as 'en' | 'de';
const buffer = await generateDatasheetPdfBuffer({ product, locale });
const fileName = generateFileName(product, locale);
// Determine subfolder based on voltage type
const voltageType = (product as any).voltageType || 'other';
const subfolder = path.join(CONFIG.outputDir, voltageType);
// Create subfolder if it doesn't exist
if (!fs.existsSync(subfolder)) {
fs.mkdirSync(subfolder, { recursive: true });
}
fs.writeFileSync(path.join(subfolder, fileName), buffer);
console.log(`${locale.toUpperCase()}: ${voltageType}/${fileName}`);
await new Promise((resolve) => setTimeout(resolve, 25));
} catch (error) {
console.error(`✗ Failed to process product ${product.id}:`, error);
}
}
}
async function processProductsInChunks(): Promise<void> {
console.log('Starting PDF generation (React-PDF)');
ensureOutputDir();
const onlyLocale = normalizeValue(String(process.env.PDF_LOCALE || '')).toLowerCase();
const locales: Array<'en' | 'de'> =
onlyLocale === 'de' || onlyLocale === 'en' ? [onlyLocale] : ['en', 'de'];
const allProducts: ProductData[] = [];
for (const locale of locales) {
const products = await loadProductsFromExcelAndCms(locale);
allProducts.push(...products);
}
if (allProducts.length === 0) {
console.log('No products found');
return;
}
// Dev convenience: generate only one product subset.
// IMPORTANT: apply filters BEFORE PDF_LIMIT so the limit works within the filtered set.
let products = allProducts;
const match = normalizeValue(String(process.env.PDF_MATCH || '')).toLowerCase();
if (match) {
products = products.filter((p) => {
const hay = [p.slug, p.translationKey, p.sku, p.name].filter(Boolean).join(' ').toLowerCase();
return hay.includes(match);
});
}
const limit = Number(process.env.PDF_LIMIT || '0');
products = Number.isFinite(limit) && limit > 0 ? products.slice(0, limit) : products;
const enProducts = products.filter((p) => (p.locale || 'en') === 'en');
const deProducts = products.filter((p) => (p.locale || 'en') === 'de');
console.log(`Found ${enProducts.length} EN + ${deProducts.length} DE products`);
const totalChunks = Math.ceil(products.length / CONFIG.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const chunk = products.slice(i * CONFIG.chunkSize, (i + 1) * CONFIG.chunkSize);
await processChunk(chunk, i, totalChunks);
}
console.log('\n✅ PDF generation completed!');
console.log(`Generated ${enProducts.length} EN + ${deProducts.length} DE PDFs`);
console.log(`Output: ${CONFIG.outputDir}`);
}
async function main(): Promise<void> {
const start = Date.now();
try {
await processProductsInChunks();
console.log(`\nTime: ${((Date.now() - start) / 1000).toFixed(2)}s`);
} catch (error) {
console.error('Fatal error:', error);
process.exit(1);
}
}
main().catch(console.error);
export { main as generatePDFDatasheets };

View File

@@ -0,0 +1,80 @@
import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
const CONTENT_DIR = path.join(process.cwd(), 'content');
const ASSETS_DIR = path.join(process.cwd(), 'public', 'media', 'visual-links');
async function downloadImage(url: string, filename: string): Promise<string> {
const filepath = path.join(ASSETS_DIR, filename);
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
await fs.writeFile(filepath, response.data);
return `/media/visual-links/${filename}`;
} catch (err: any) {
console.error(`Failed to download ${url}:`, err.message);
throw err;
}
}
async function processMdxFile(filepath: string) {
let content = await fs.readFile(filepath, 'utf-8');
let modified = false;
const linkPreviewRegex = /<Block type="visualLinkPreview" data={([^}]+)}} \/>/g;
let match;
let newContent = content;
// Need to correctly parse the JSON inside data={...}
// The regex above matches `<Block type="visualLinkPreview" data={{"url":"...","image":"..."}} />`
// Actually, we can just regex for "image":"https://..."
const imageRegex = /"image":\s*"([^"]+)"/g;
while ((match = linkPreviewRegex.exec(content)) !== null) {
const dataString = match[1] + '}';
try {
const data = JSON.parse(dataString);
if (data.image && data.image.startsWith('http')) {
const url = new URL(data.image);
let ext = path.extname(url.pathname);
if (!ext) ext = '.jpg'; // Fallback
const filename = `${data.id || Math.random().toString(36).substring(7)}${ext}`;
console.log(`Downloading ${data.image} to ${filename}...`);
const newUrl = await downloadImage(data.image, filename);
const oldImageString = `"image":"${data.image}"`;
const newImageString = `"image":"${newUrl}"`;
newContent = newContent.replace(oldImageString, newImageString);
modified = true;
}
} catch (err) {
console.warn(`Could not parse data string in ${filepath}:`, dataString);
}
}
if (modified) {
await fs.writeFile(filepath, newContent, 'utf-8');
console.log(`✅ Updated ${filepath}`);
}
}
async function walk(dir: string) {
const files = await fs.readdir(dir);
for (const file of files) {
const filepath = path.join(dir, file);
const stat = await fs.stat(filepath);
if (stat.isDirectory()) {
await walk(filepath);
} else if (file.endsWith('.mdx')) {
await processMdxFile(filepath);
}
}
}
async function main() {
await fs.mkdir(ASSETS_DIR, { recursive: true });
await walk(CONTENT_DIR);
}
main().catch(console.error);

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env bash
# ────────────────────────────────────────────────────────────────────────────
# Payload CMS Database Restore
# Restores a backup created by backup-db.sh
# Usage: npm run restore:db -- backups/payload_20260224_191900.sql.gz
# ────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# Load environment variables
if [ -f .env ]; then
set -a; source .env; set +a
fi
DB_NAME="${PAYLOAD_DB_NAME:-payload}"
DB_USER="${PAYLOAD_DB_USER:-payload}"
DB_CONTAINER="klz-2026-klz-db-1"
BACKUP_FILE="${1:-}"
if [ -z "$BACKUP_FILE" ]; then
echo "❌ Usage: npm run restore:db -- <backup-file>"
echo ""
echo "📋 Available backups:"
ls -lh backups/*.sql.gz 2>/dev/null | awk '{print " " $NF " (" $5 ")"}' || echo " No backups found."
exit 1
fi
if [ ! -f "$BACKUP_FILE" ]; then
echo "❌ Backup file not found: $BACKUP_FILE"
exit 1
fi
# Check if container is running
if ! docker ps --format '{{.Names}}' | grep -q "$DB_CONTAINER"; then
echo "❌ Database container '$DB_CONTAINER' is not running."
echo " Start it with: docker compose up -d klz-db"
exit 1
fi
echo "⚠️ WARNING: This will REPLACE ALL DATA in the '$DB_NAME' database!"
echo " Backup file: $BACKUP_FILE"
echo ""
read -p "Are you sure? (y/N) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
echo "🔄 Restoring database from $BACKUP_FILE..."
gunzip -c "$BACKUP_FILE" | docker exec -i "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" --quiet
echo "✅ Database restored successfully!"

View File

@@ -1,131 +0,0 @@
/**
* Migration: Seed homepage ('start') as Lexical block content into Payload CMS.
*
* Usage:
* pnpm tsx scripts/seed-home.ts
*/
import { getPayload } from 'payload';
import configPromise from '@payload-config';
function lexicalBlock(blockType: string, fields: Record<string, any> = {}) {
return {
type: 'block',
version: 2,
fields: {
blockType,
...fields,
},
};
}
function lexicalDoc(blocks: any[]) {
return {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: blocks,
direction: 'ltr',
},
};
}
const PAGES = [
// ─── Homepage (DE) ─────────────────────────────────────────
{
title: 'Startseite',
slug: 'start',
locale: 'de',
layout: 'fullBleed',
excerpt: 'Ihr Experte für hochwertige Stromkabel, Mittelspannungslösungen und Solarkabel. Zuverlässige Infrastruktur für eine grüne Energiezukunft.',
_status: 'published',
content: lexicalDoc([
lexicalBlock('homeHero', { note: 'Hero section with primary video and CTA.' }),
lexicalBlock('homeProductCategories', { note: 'Product categories overview based on CMS data.' }),
lexicalBlock('homeWhatWeDo', { note: 'What we do / capabilities overview.' }),
lexicalBlock('homeRecentPosts', { note: 'Latest 3 blog articles snippet.' }),
lexicalBlock('homeExperience', { note: 'Experience and history timeline snippet.' }),
lexicalBlock('homeWhyChooseUs', { note: 'Why choose KLZ Cables metrics and selling points.' }),
lexicalBlock('homeMeetTheTeam', { note: 'High-level Meet the Team teaser.' }),
lexicalBlock('homeGallery', { note: 'Image gallery from our facilities.' }),
lexicalBlock('homeVideo', { note: 'Secondary informative background video.' }),
lexicalBlock('homeCTA', { note: 'Bottom call to action linking to contact.' }),
]),
},
// ─── Homepage (EN) ─────────────────────────────────────────
{
title: 'Homepage',
slug: 'start',
locale: 'en',
layout: 'fullBleed',
excerpt: 'Your expert for high-quality power cables, medium voltage solutions, and solar cables. Reliable infrastructure for a green energy future.',
_status: 'published',
content: lexicalDoc([
lexicalBlock('homeHero', { note: 'Hero section with primary video and CTA.' }),
lexicalBlock('homeProductCategories', { note: 'Product categories overview based on CMS data.' }),
lexicalBlock('homeWhatWeDo', { note: 'What we do / capabilities overview.' }),
lexicalBlock('homeRecentPosts', { note: 'Latest 3 blog articles snippet.' }),
lexicalBlock('homeExperience', { note: 'Experience and history timeline snippet.' }),
lexicalBlock('homeWhyChooseUs', { note: 'Why choose KLZ Cables metrics and selling points.' }),
lexicalBlock('homeMeetTheTeam', { note: 'High-level Meet the Team teaser.' }),
lexicalBlock('homeGallery', { note: 'Image gallery from our facilities.' }),
lexicalBlock('homeVideo', { note: 'Secondary informative background video.' }),
lexicalBlock('homeCTA', { note: 'Bottom call to action linking to contact.' }),
]),
},
];
async function seedHome() {
const payload = await getPayload({ config: configPromise });
for (const page of PAGES) {
const existing = await payload.find({
collection: 'pages',
where: {
slug: { equals: page.slug },
locale: { equals: page.locale },
},
limit: 1,
});
const docs = existing.docs as any[];
if (docs.length > 0) {
await payload.update({
collection: 'pages',
id: docs[0].id,
data: {
title: page.title,
layout: page.layout as any,
excerpt: page.excerpt,
_status: page._status as any,
content: page.content as any,
},
});
console.log(`✅ Updated: ${page.slug} (${page.locale})`);
} else {
await payload.create({
collection: 'pages',
data: {
title: page.title,
slug: page.slug,
locale: page.locale,
layout: page.layout as any,
excerpt: page.excerpt,
_status: page._status as any,
content: page.content as any,
},
});
console.log(`✅ Created: ${page.slug} (${page.locale})`);
}
}
console.log('\n🎉 Homepage seeded successfully!');
process.exit(0);
}
seedHome().catch((err) => {
console.error('❌ Seed failed:', err);
process.exit(1);
});

View File

@@ -1,242 +0,0 @@
/**
* Migration: Seed team, contact, and other missing pages as Lexical block content into Payload CMS.
*
* Usage:
* pnpm tsx scripts/seed-pages.ts
*/
import { getPayload } from 'payload';
import configPromise from '@payload-config';
function lexicalBlock(blockType: string, fields: Record<string, any>) {
return {
type: 'block',
version: 2,
fields: {
blockType,
...fields,
},
};
}
function lexicalDoc(blocks: any[]) {
return {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: blocks,
direction: 'ltr',
},
};
}
const PAGES = [
// ─── Team (DE) ────────────────────────────────────────────
{
title: 'Team',
slug: 'team',
locale: 'de',
layout: 'fullBleed',
excerpt: '',
_status: 'published',
content: lexicalDoc([
lexicalBlock('heroSection', {
badge: 'Das Team',
title: 'Tradition trifft Moderne',
subtitle: 'Zwei Generationen, eine Vision: Deutschlands zuverlässigster Partner für Kabel & Leitungen.',
alignment: 'center',
}),
lexicalBlock('teamProfile', {
name: 'Michael Bodemer',
role: 'Geschäftsführer',
quote: 'Innovation entsteht dort, wo Erfahrung auf frische Ideen trifft.',
description: 'Als Geschäftsführer verbindet Michael jahrzehntelange Branchenexpertise mit einem klaren Blick für die Zukunft. Sein Fokus liegt auf nachhaltigen Lösungen und modernster Technologie.',
linkedinUrl: 'https://www.linkedin.com/in/michael-bodemer-33b493122/',
layout: 'imageRight',
colorScheme: 'dark',
}),
lexicalBlock('stats', {
stats: [
{ value: '30+', label: 'Jahre Expertise' },
{ value: 'Global', label: 'Netzwerk' },
],
}),
lexicalBlock('teamProfile', {
name: 'Klaus Mintel',
role: 'Gründer & Berater',
quote: 'Qualität ist kein Zufall sie ist das Ergebnis von Engagement und Erfahrung.',
description: 'Klaus gründete KLZ Cables und hat das Unternehmen zu einem der zuverlässigsten Partner der Kabelindustrie aufgebaut. Er bringt Jahrzehnte an Expertise ein.',
linkedinUrl: 'https://www.linkedin.com/in/klaus-mintel-b80a8b193/',
layout: 'imageLeft',
colorScheme: 'light',
}),
lexicalBlock('manifestoGrid', {
title: 'Unsere Werte',
subtitle: 'Was uns antreibt',
tagline: 'Seit der Gründung leiten uns klare Prinzipien, die wir jeden Tag leben.',
items: [
{ title: 'Qualität', description: 'Wir liefern nur Produkte, die höchsten Standards entsprechen.' },
{ title: 'Zuverlässigkeit', description: 'Termingerechte Lieferung ist für uns selbstverständlich.' },
{ title: 'Partnerschaft', description: 'Langfristige Beziehungen sind die Grundlage unseres Erfolgs.' },
{ title: 'Innovation', description: 'Wir investieren in neue Technologien und nachhaltige Lösungen.' },
{ title: 'Transparenz', description: 'Offene Kommunikation und faire Preise zeichnen uns aus.' },
{ title: 'Nachhaltigkeit', description: 'Verantwortung für Umwelt und Gesellschaft ist Teil unserer DNA.' },
],
}),
// Removed the imageGallery since it requires at least 1 image and we don't have media upload IDs yet.
]),
},
// ─── Team (EN) ────────────────────────────────────────────
{
title: 'Team',
slug: 'team',
locale: 'en',
layout: 'fullBleed',
excerpt: '',
_status: 'published',
content: lexicalDoc([
lexicalBlock('heroSection', {
badge: 'The Team',
title: 'Tradition Meets Innovation',
subtitle: 'Two generations, one vision: Germany\'s most reliable partner for cables & wiring.',
alignment: 'center',
}),
lexicalBlock('teamProfile', {
name: 'Michael Bodemer',
role: 'Managing Director',
quote: 'Innovation happens where experience meets fresh ideas.',
description: 'As Managing Director, Michael combines decades of industry expertise with a clear vision for the future. His focus is on sustainable solutions and cutting-edge technology.',
linkedinUrl: 'https://www.linkedin.com/in/michael-bodemer-33b493122/',
layout: 'imageRight',
colorScheme: 'dark',
}),
lexicalBlock('stats', {
stats: [
{ value: '30+', label: 'Years of Expertise' },
{ value: 'Global', label: 'Network' },
],
}),
lexicalBlock('teamProfile', {
name: 'Klaus Mintel',
role: 'Founder & Advisor',
quote: 'Quality is no accident it is the result of commitment and experience.',
description: 'Klaus founded KLZ Cables and built the company into one of the most reliable partners in the cable industry. He brings decades of expertise.',
linkedinUrl: 'https://www.linkedin.com/in/klaus-mintel-b80a8b193/',
layout: 'imageLeft',
colorScheme: 'light',
}),
lexicalBlock('manifestoGrid', {
title: 'Our Values',
subtitle: 'What drives us',
tagline: 'Since our founding, clear principles have guided us every day.',
items: [
{ title: 'Quality', description: 'We only deliver products that meet the highest standards.' },
{ title: 'Reliability', description: 'On-time delivery is our standard.' },
{ title: 'Partnership', description: 'Long-term relationships are the foundation of our success.' },
{ title: 'Innovation', description: 'We invest in new technologies and sustainable solutions.' },
{ title: 'Transparency', description: 'Open communication and fair pricing define us.' },
{ title: 'Sustainability', description: 'Responsibility for the environment and society is part of our DNA.' },
],
}),
]),
},
// ─── Contact (DE) ─────────────────────────────────────────
{
title: 'Kontakt',
slug: 'kontakt',
locale: 'de',
layout: 'fullBleed',
excerpt: '',
_status: 'published',
content: lexicalDoc([
lexicalBlock('heroSection', {
badge: 'Kontakt',
title: 'Sprechen Sie mit uns',
subtitle: 'Wir sind für Sie da. Kontaktieren Sie uns für Beratung, Angebote oder technische Fragen.',
alignment: 'left',
}),
lexicalBlock('contactSection', {
showForm: true,
showMap: true,
showHours: true,
}),
]),
},
// ─── Contact (EN) ─────────────────────────────────────────
{
title: 'Contact',
slug: 'contact',
locale: 'en',
layout: 'fullBleed',
excerpt: '',
_status: 'published',
content: lexicalDoc([
lexicalBlock('heroSection', {
badge: 'Contact',
title: 'Talk to us',
subtitle: 'We are here for you. Contact us for consulting, quotes, or technical questions.',
alignment: 'left',
}),
lexicalBlock('contactSection', {
showForm: true,
showMap: true,
showHours: true,
}),
]),
},
];
async function seedPages() {
const payload = await getPayload({ config: configPromise });
for (const page of PAGES) {
const existing = await payload.find({
collection: 'pages',
where: {
slug: { equals: page.slug },
locale: { equals: page.locale },
},
limit: 1,
});
const docs = existing.docs as any[];
if (docs.length > 0) {
await payload.update({
collection: 'pages',
id: docs[0].id,
locale: page.locale,
data: {
title: page.title,
layout: page.layout as any,
_status: page._status as any,
content: page.content as any,
},
});
console.log(`✅ Updated: ${page.slug} (${page.locale})`);
} else {
await payload.create({
collection: 'pages',
locale: page.locale,
data: {
title: page.title,
slug: page.slug,
layout: page.layout as any,
excerpt: page.excerpt,
_status: page._status as any,
content: page.content as any,
},
});
console.log(`✅ Created: ${page.slug} (${page.locale})`);
}
}
console.log('\n🎉 All pages seeded successfully!');
process.exit(0);
}
seedPages().catch((err) => {
console.error('❌ Seed failed:', err);
process.exit(1);
});

View File

@@ -1,22 +0,0 @@
/**
* CLI wrapper for seeding the Payload CMS database.
* Usage: pnpm tsx scripts/seed-payload.ts
*/
import { getPayload } from 'payload';
import configPromise from '../payload.config';
import { seedDatabase } from '../src/payload/seed';
async function run() {
const payload = await getPayload({ config: configPromise });
console.log('🌱 Starting database seed...');
await seedDatabase(payload);
console.log('✅ Seeding complete.');
process.exit(0);
}
run().catch((err) => {
console.error('❌ Seeding failed:', err);
process.exit(1);
});

View File

@@ -1,14 +0,0 @@
import { getPayload } from 'payload';
import configPromise from '@payload-config';
async function run() {
const payload = await getPayload({ config: configPromise });
const existing = await payload.find({
collection: 'pages',
where: { slug: { equals: 'start' } },
limit: 1,
});
console.log('Homepage blocks found:', existing.docs[0].content?.root?.children?.length);
process.exit(0);
}
run();

View File

@@ -1,37 +0,0 @@
import { getPayload } from 'payload';
import configPromise from '../payload.config';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
async function run() {
process.env.PAYLOAD_MIGRATE = 'false';
process.env.PAYLOAD_MIGRATE_SKIP_PROMPTS = 'true';
const payload = await getPayload({ config: configPromise });
console.log('📖 Reading lexical_avb.json...');
const lexicalContent = JSON.parse(fs.readFileSync(path.resolve(dirname, '../lexical_avb.json'), 'utf8'));
console.log('🚀 Updating AGB page (ID 6) to AVB...');
const updatedPage = await payload.update({
collection: 'pages',
id: 6,
data: {
title: 'Allgemeine Verkaufsbedingungen (AVB)',
excerpt: '',
content: lexicalContent,
},
});
console.log('✅ Page updated successfully:', updatedPage.title);
process.exit(0);
}
run().catch((err) => {
console.error('❌ Update failed:', err);
process.exit(1);
});