initial migration

This commit is contained in:
2025-12-28 23:28:31 +01:00
parent 1f99781458
commit 292975299d
284 changed files with 119466 additions and 0 deletions

706
scripts/wordpress-export.js Executable file
View File

@@ -0,0 +1,706 @@
#!/usr/bin/env node
/**
* WordPress to Next.js Data Export Script
* Gathers all required data from WordPress/WooCommerce for static site generation
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
// Load environment variables
require('dotenv').config();
const BASE_URL = process.env.WOOCOMMERCE_URL;
const CONSUMER_KEY = process.env.WOOCOMMERCE_CONSUMER_KEY;
const CONSUMER_SECRET = process.env.WOOCOMMERCE_CONSUMER_SECRET;
const APP_PASSWORD = process.env.WORDPRESS_APP_PASSWORD;
// Validate environment
if (!BASE_URL || !CONSUMER_KEY || !CONSUMER_SECRET) {
console.error('❌ Missing required environment variables');
console.error('Please check .env file for:');
console.error(' - WOOCOMMERCE_URL');
console.error(' - WOOCOMMERCE_CONSUMER_KEY');
console.error(' - WOOCOMMERCE_CONSUMER_SECRET');
process.exit(1);
}
// Configuration
const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-');
const OUTPUT_DIR = path.join(__dirname, '..', 'data', 'raw', TIMESTAMP);
const MEDIA_DIR = path.join(__dirname, '..', 'public', 'media');
// Create output directories
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
if (!fs.existsSync(MEDIA_DIR)) {
fs.mkdirSync(MEDIA_DIR, { recursive: true });
}
// API Helper Functions
function buildAuthHeader() {
const credentials = Buffer.from(`${CONSUMER_KEY}:${CONSUMER_SECRET}`).toString('base64');
return `Basic ${credentials}`;
}
function buildWordPressAuth() {
// For WordPress REST API with app password
return {
'Authorization': `Basic ${Buffer.from(`admin:${APP_PASSWORD}`).toString('base64')}`,
'Content-Type': 'application/json'
};
}
function makeRequest(url, headers = {}) {
return new Promise((resolve, reject) => {
const options = {
headers: {
'User-Agent': 'WordPress-NextJS-Migration/1.0',
...headers
}
};
https.get(url, options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data));
} catch (e) {
resolve(data);
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
}).on('error', reject);
});
}
async function fetchWithPagination(endpoint, params = {}, locale = null) {
const allItems = [];
let page = 1;
const perPage = 100;
while (true) {
const queryString = new URLSearchParams({
...params,
page: page.toString(),
per_page: perPage.toString(),
...(locale ? { lang: locale } : {})
}).toString();
const url = `${BASE_URL}/wp-json/wp/v2/${endpoint}?${queryString}`;
console.log(`📥 Fetching ${endpoint} page ${page}${locale ? ` (${locale})` : ''}...`);
try {
const items = await makeRequest(url, buildWordPressAuth());
if (!Array.isArray(items) || items.length === 0) {
break;
}
allItems.push(...items);
// Check if we got a full page (indicates more pages might exist)
if (items.length < perPage) {
break;
}
page++;
} catch (error) {
console.error(`❌ Error fetching ${endpoint} page ${page}:`, error.message);
break;
}
}
return allItems;
}
async function fetchWooCommerce(endpoint, params = {}, locale = null) {
const queryString = new URLSearchParams({
...params,
per_page: '100',
...(locale ? { lang: locale } : {})
}).toString();
const url = `${BASE_URL}/wp-json/wc/v3/${endpoint}?${queryString}`;
console.log(`📥 Fetching WooCommerce ${endpoint}${locale ? ` (${locale})` : ''}...`);
try {
const response = await makeRequest(url, {
'Authorization': buildAuthHeader(),
'Content-Type': 'application/json'
});
return Array.isArray(response) ? response : [response];
} catch (error) {
console.error(`❌ Error fetching WooCommerce ${endpoint}:`, error.message);
return [];
}
}
async function fetchMedia(mediaId) {
const url = `${BASE_URL}/wp-json/wp/v2/media/${mediaId}`;
try {
const media = await makeRequest(url, buildWordPressAuth());
return media;
} catch (error) {
console.error(`❌ Error fetching media ${mediaId}:`, error.message);
return null;
}
}
async function downloadMedia(url, filename) {
return new Promise((resolve, reject) => {
const filePath = path.join(MEDIA_DIR, filename);
// Check if file already exists
if (fs.existsSync(filePath)) {
console.log(`✅ Media already downloaded: ${filename}`);
resolve(filePath);
return;
}
const file = fs.createWriteStream(filePath);
https.get(url, (res) => {
if (res.statusCode === 200) {
res.pipe(file);
file.on('finish', () => {
console.log(`✅ Downloaded: ${filename}`);
resolve(filePath);
});
} else {
reject(new Error(`Failed to download: ${res.statusCode}`));
}
}).on('error', (err) => {
fs.unlink(filePath, () => {});
reject(err);
});
});
}
// Data Processing Functions
function extractFeaturedImage(item) {
if (item.featured_media) {
return item.featured_media;
}
if (item._embedded && item._embedded['wp:featuredmedia']) {
return item._embedded['wp:featuredmedia'][0];
}
return null;
}
function processPage(page, locale) {
return {
id: page.id,
translationKey: `page-${page.slug}`, // Will be refined with Polylang data
locale: locale,
slug: page.slug,
path: locale === 'en' ? `/${page.slug}` : `/${locale}/${page.slug}`,
titleHtml: page.title?.rendered || '',
contentHtml: page.content?.rendered || '',
excerptHtml: page.excerpt?.rendered || '',
featuredImage: page.featured_media || null,
updatedAt: page.modified || page.date
};
}
function processPost(post, locale) {
return {
id: post.id,
translationKey: `post-${post.slug}`,
locale: locale,
slug: post.slug,
path: locale === 'en' ? `/blog/${post.slug}` : `/${locale}/blog/${post.slug}`,
titleHtml: post.title?.rendered || '',
contentHtml: post.content?.rendered || '',
excerptHtml: post.excerpt?.rendered || '',
featuredImage: post.featured_media || null,
datePublished: post.date,
updatedAt: post.modified || post.date
};
}
function processProduct(product, locale) {
return {
id: product.id,
translationKey: `product-${product.slug}`,
locale: locale,
slug: product.slug,
path: locale === 'en' ? `/product/${product.slug}` : `/${locale}/product/${product.slug}`,
name: product.name,
shortDescriptionHtml: product.short_description || '',
descriptionHtml: product.description || '',
images: product.images ? product.images.map(img => img.src) : [],
featuredImage: product.images && product.images.length > 0 ? product.images[0].src : null,
sku: product.sku,
regularPrice: product.regular_price,
salePrice: product.sale_price,
currency: product.currency || 'EUR',
stockStatus: product.stock_status,
categories: product.categories ? product.categories.map(cat => ({ id: cat.id, name: cat.name, slug: cat.slug })) : [],
attributes: product.attributes || [],
variations: product.variations || [],
updatedAt: product.date_modified
};
}
function processProductCategory(category, locale) {
return {
id: category.id,
translationKey: `product-category-${category.slug}`,
locale: locale,
slug: category.slug,
name: category.name,
path: locale === 'en' ? `/product-category/${category.slug}` : `/${locale}/product-category/${category.slug}`,
description: category.description || '',
count: category.count || 0
};
}
function processMenu(menu, locale) {
// WordPress menus are complex, we'll extract basic structure
return {
id: menu.term_id || menu.id,
slug: menu.slug,
name: menu.name,
locale: locale,
items: menu.items || []
};
}
// Main Export Functions
async function exportPages() {
console.log('\n📊 EXPORTING PAGES');
const pagesEN = await fetchWithPagination('pages', { status: 'publish' }, 'en');
const pagesDE = await fetchWithPagination('pages', { status: 'publish' }, 'de');
const processedEN = pagesEN.map(p => processPage(p, 'en'));
const processedDE = pagesDE.map(p => processPage(p, 'de'));
fs.writeFileSync(
path.join(OUTPUT_DIR, 'pages.en.json'),
JSON.stringify(processedEN, null, 2)
);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'pages.de.json'),
JSON.stringify(processedDE, null, 2)
);
console.log(`✅ Pages: ${processedEN.length} EN, ${processedDE.length} DE`);
return { en: processedEN, de: processedDE };
}
async function exportPosts() {
console.log('\n📊 EXPORTING POSTS');
const postsEN = await fetchWithPagination('posts', { status: 'publish' }, 'en');
const postsDE = await fetchWithPagination('posts', { status: 'publish' }, 'de');
const processedEN = postsEN.map(p => processPost(p, 'en'));
const processedDE = postsDE.map(p => processPost(p, 'de'));
fs.writeFileSync(
path.join(OUTPUT_DIR, 'posts.en.json'),
JSON.stringify(processedEN, null, 2)
);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'posts.de.json'),
JSON.stringify(processedDE, null, 2)
);
console.log(`✅ Posts: ${processedEN.length} EN, ${processedDE.length} DE`);
return { en: processedEN, de: processedDE };
}
async function exportProducts() {
console.log('\n📊 EXPORTING PRODUCTS');
const productsEN = await fetchWooCommerce('products', {}, 'en');
const productsDE = await fetchWooCommerce('products', {}, 'de');
const processedEN = productsEN.map(p => processProduct(p, 'en'));
const processedDE = productsDE.map(p => processProduct(p, 'de'));
fs.writeFileSync(
path.join(OUTPUT_DIR, 'products.en.json'),
JSON.stringify(processedEN, null, 2)
);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'products.de.json'),
JSON.stringify(processedDE, null, 2)
);
console.log(`✅ Products: ${processedEN.length} EN, ${processedDE.length} DE`);
return { en: processedEN, de: processedDE };
}
async function exportProductCategories() {
console.log('\n📊 EXPORTING PRODUCT CATEGORIES');
const categoriesEN = await fetchWooCommerce('products/categories', {}, 'en');
const categoriesDE = await fetchWooCommerce('products/categories', {}, 'de');
const processedEN = categoriesEN.map(c => processProductCategory(c, 'en'));
const processedDE = categoriesDE.map(c => processProductCategory(c, 'de'));
fs.writeFileSync(
path.join(OUTPUT_DIR, 'product-categories.en.json'),
JSON.stringify(processedEN, null, 2)
);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'product-categories.de.json'),
JSON.stringify(processedDE, null, 2)
);
console.log(`✅ Product Categories: ${processedEN.length} EN, ${processedDE.length} DE`);
return { en: processedEN, de: processedDE };
}
async function exportMenus() {
console.log('\n📊 EXPORTING MENUS');
// Try to get menus via WordPress REST API
// Note: This might require additional plugins or direct DB access
const menusEN = await fetchWithPagination('menus', {}, 'en').catch(() => []);
const menusDE = await fetchWithPagination('menus', {}, 'de').catch(() => []);
// If menus endpoint doesn't work, try to get menu locations
let menuLocations = {};
try {
const locations = await makeRequest(`${BASE_URL}/wp-json/wp/v2/menu-locations`, buildWordPressAuth());
menuLocations = locations;
} catch (e) {
console.log('⚠️ Menu locations endpoint not available');
}
const processedEN = menusEN.map(m => processMenu(m, 'en'));
const processedDE = menusDE.map(m => processMenu(m, 'de'));
fs.writeFileSync(
path.join(OUTPUT_DIR, 'menus.en.json'),
JSON.stringify({ menus: processedEN, locations: menuLocations }, null, 2)
);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'menus.de.json'),
JSON.stringify({ menus: processedDE, locations: menuLocations }, null, 2)
);
console.log(`✅ Menus: ${processedEN.length} EN, ${processedDE.length} DE`);
return { en: processedEN, de: processedDE, locations: menuLocations };
}
async function exportMedia() {
console.log('\n📊 EXPORTING MEDIA');
// Get all unique media IDs from collected data
const mediaIds = new Set();
// Read all JSON files to find media references
const jsonFiles = fs.readdirSync(OUTPUT_DIR).filter(f => f.endsWith('.json'));
for (const file of jsonFiles) {
const content = JSON.parse(fs.readFileSync(path.join(OUTPUT_DIR, file), 'utf8'));
const items = Array.isArray(content) ? content : (content.menus || []);
items.forEach(item => {
if (item.featuredImage) mediaIds.add(item.featuredImage);
if (item.images) item.images.forEach(img => {
// Extract ID from URL if possible, or add as URL
if (typeof img === 'string' && img.includes('/wp-content/')) {
mediaIds.add(img);
}
});
});
}
const mediaManifest = [];
const downloadPromises = [];
for (const mediaRef of mediaIds) {
if (typeof mediaRef === 'number') {
// Fetch media info
const media = await fetchMedia(mediaRef);
if (media && media.source_url) {
const filename = `${mediaRef}-${path.basename(media.source_url)}`;
mediaManifest.push({
id: mediaRef,
url: media.source_url,
filename: filename,
alt: media.alt_text || '',
width: media.media_details?.width,
height: media.media_details?.height,
mime_type: media.mime_type
});
// Download file
downloadPromises.push(
downloadMedia(media.source_url, filename).catch(err => {
console.warn(`⚠️ Failed to download media ${mediaRef}:`, err.message);
})
);
}
} else if (typeof mediaRef === 'string' && mediaRef.startsWith('http')) {
// Direct URL
const filename = `media-${Date.now()}-${path.basename(mediaRef)}`;
mediaManifest.push({
id: null,
url: mediaRef,
filename: filename,
alt: '',
width: null,
height: null,
mime_type: null
});
downloadPromises.push(
downloadMedia(mediaRef, filename).catch(err => {
console.warn(`⚠️ Failed to download media from URL:`, err.message);
})
);
}
}
// Wait for all downloads
await Promise.all(downloadPromises);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'media.json'),
JSON.stringify(mediaManifest, null, 2)
);
console.log(`✅ Media: ${mediaManifest.length} items`);
return mediaManifest;
}
async function exportSiteInfo() {
console.log('\n📊 EXPORTING SITE INFORMATION');
const siteInfo = {
baseUrl: BASE_URL,
exportDate: new Date().toISOString(),
timestamp: TIMESTAMP,
polylang: false,
languages: ['en', 'de'],
defaultLocale: 'en' // Will need to confirm
};
// Check for Polylang
try {
const plugins = await makeRequest(`${BASE_URL}/wp-json/wp/v2/plugins`, buildWordPressAuth());
const polylangPlugin = plugins.find(p => p.name.includes('polylang'));
if (polylangPlugin) {
siteInfo.polylang = true;
siteInfo.polylangVersion = polylangPlugin.version;
}
} catch (e) {
console.log('⚠️ Could not check plugins');
}
// Get site settings
try {
const settings = await makeRequest(`${BASE_URL}/wp-json/wp/v2/settings`, buildWordPressAuth());
siteInfo.siteTitle = settings.title;
siteInfo.siteDescription = settings.description;
siteInfo.defaultLanguage = settings.default_language || 'en';
} catch (e) {
console.log('⚠️ Could not fetch settings');
}
// Get permalink structure
try {
const permalink = await makeRequest(`${BASE_URL}/wp-json/wp/v2/settings`, buildWordPressAuth());
siteInfo.permalinkStructure = permalink.permalink_structure;
} catch (e) {
console.log('⚠️ Could not fetch permalink structure');
}
fs.writeFileSync(
path.join(OUTPUT_DIR, 'site-info.json'),
JSON.stringify(siteInfo, null, 2)
);
console.log('✅ Site info exported');
return siteInfo;
}
async function generateTranslationMapping() {
console.log('\n📊 GENERATING TRANSLATION MAPPING');
// This function creates translationKey mappings between locales
// We'll use slug-based matching for now, but this should be enhanced with Polylang data
const mapping = {
pages: {},
posts: {},
products: {},
productCategories: {}
};
// Load all data
const loadFile = (filename) => {
try {
return JSON.parse(fs.readFileSync(path.join(OUTPUT_DIR, filename), 'utf8'));
} catch (e) {
return [];
}
};
const pagesEN = loadFile('pages.en.json');
const pagesDE = loadFile('pages.de.json');
const postsEN = loadFile('posts.en.json');
const postsDE = loadFile('posts.de.json');
const productsEN = loadFile('products.en.json');
const productsDE = loadFile('products.de.json');
const categoriesEN = loadFile('product-categories.en.json');
const categoriesDE = loadFile('product-categories.de.json');
// Helper to find translation pairs by slug
function findTranslationPairs(enItems, deItems) {
const pairs = {};
enItems.forEach(enItem => {
const deMatch = deItems.find(de => de.slug === enItem.slug);
if (deMatch) {
const translationKey = `${enItem.slug}`;
pairs[translationKey] = {
en: enItem.id,
de: deMatch.id
};
}
});
return pairs;
}
mapping.pages = findTranslationPairs(pagesEN, pagesDE);
mapping.posts = findTranslationPairs(postsEN, postsDE);
mapping.products = findTranslationPairs(productsEN, productsDE);
mapping.productCategories = findTranslationPairs(categoriesEN, categoriesDE);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'translation-mapping.json'),
JSON.stringify(mapping, null, 2)
);
const totalPairs = Object.values(mapping).reduce((sum, obj) => sum + Object.keys(obj).length, 0);
console.log(`✅ Translation mapping: ${totalPairs} pairs found`);
return mapping;
}
async function generateRedirects() {
console.log('\n📊 GENERATING REDIRECT RULES');
const redirects = [];
// Load posts
const postsEN = JSON.parse(fs.readFileSync(path.join(OUTPUT_DIR, 'posts.en.json'), 'utf8'));
const postsDE = JSON.parse(fs.readFileSync(path.join(OUTPUT_DIR, 'posts.de.json'), 'utf8'));
// Base redirect: /{postSlug} → /blog/{postSlug} (English)
postsEN.forEach(post => {
redirects.push({
source: `/${post.slug}`,
destination: `/blog/${post.slug}`,
permanent: true,
locale: 'en'
});
});
// German redirects: /de/{postSlug} → /de/blog/{postSlug}
postsDE.forEach(post => {
redirects.push({
source: `/de/${post.slug}`,
destination: `/de/blog/${post.slug}`,
permanent: true,
locale: 'de'
});
});
fs.writeFileSync(
path.join(OUTPUT_DIR, 'redirects.json'),
JSON.stringify(redirects, null, 2)
);
console.log(`✅ Redirects: ${redirects.length} rules generated`);
return redirects;
}
// Main Execution
async function main() {
console.log('🚀 WordPress → Next.js Data Export');
console.log('=====================================');
console.log(`Target: ${BASE_URL}`);
console.log(`Output: ${OUTPUT_DIR}`);
console.log('');
try {
// Step 1: Export all content
await exportSiteInfo();
await exportPages();
await exportPosts();
await exportProducts();
await exportProductCategories();
await exportMenus();
await exportMedia();
// Step 2: Generate mappings and redirects
await generateTranslationMapping();
await generateRedirects();
console.log('\n🎉 Export Complete!');
console.log('=====================================');
console.log(`📁 Data directory: data/raw/${TIMESTAMP}`);
console.log(`🖼️ Media directory: public/media/`);
console.log('');
console.log('Next steps:');
console.log('1. Review exported data for completeness');
console.log('2. Check for any missing translations');
console.log('3. Verify media downloads');
console.log('4. Proceed with Next.js data processing');
} catch (error) {
console.error('\n❌ Export failed:', error.message);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = {
exportPages,
exportPosts,
exportProducts,
exportProductCategories,
exportMenus,
exportMedia,
exportSiteInfo,
generateTranslationMapping,
generateRedirects
};