feat: migrate Payload CMS to MDX and harden static infrastructure
This commit is contained in:
@@ -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 ")"}'
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
|
||||
125
scripts/convert-lexical-to-md.ts
Normal file
125
scripts/convert-lexical-to-md.ts
Normal 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 `\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
26
scripts/fix-media-urls.ts
Normal 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.`);
|
||||
@@ -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 };
|
||||
80
scripts/migrate-visual-links.ts
Normal file
80
scripts/migrate-visual-links.ts
Normal 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);
|
||||
@@ -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!"
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user