996 lines
30 KiB
JavaScript
Executable File
996 lines
30 KiB
JavaScript
Executable File
#!/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');
|
|
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function downloadFavicon(url, filename) {
|
|
return new Promise((resolve, reject) => {
|
|
const filePath = path.join(PUBLIC_DIR, filename);
|
|
|
|
// Check if file already exists
|
|
if (fs.existsSync(filePath)) {
|
|
console.log(`✅ Favicon already exists: ${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 favicon: ${filename}`);
|
|
resolve(filePath);
|
|
});
|
|
} else {
|
|
reject(new Error(`Failed to download favicon: ${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 exportLogoAndFavicon() {
|
|
console.log('\n📊 EXPORTING LOGO AND FAVICON');
|
|
|
|
const assets = {
|
|
logo: null,
|
|
logoSvg: null,
|
|
favicon: null,
|
|
appleTouchIcon: null,
|
|
siteIconId: null
|
|
};
|
|
|
|
try {
|
|
// Get site settings which may include logo and icon IDs
|
|
const settings = await makeRequest(`${BASE_URL}/wp-json/wp/v2/settings`, buildWordPressAuth());
|
|
|
|
// Try to get custom_logo
|
|
if (settings.custom_logo) {
|
|
console.log(`📥 Found custom_logo ID: ${settings.custom_logo}`);
|
|
const logoMedia = await fetchMedia(settings.custom_logo);
|
|
if (logoMedia && logoMedia.source_url) {
|
|
const ext = path.extname(logoMedia.source_url);
|
|
const logoFilename = `logo${ext}`;
|
|
await downloadMedia(logoMedia.source_url, logoFilename);
|
|
assets.logo = `/media/${logoFilename}`;
|
|
console.log(`✅ Logo downloaded: ${logoFilename}`);
|
|
|
|
// Check if it's SVG
|
|
if (logoMedia.mime_type === 'image/svg+xml' || ext === '.svg') {
|
|
assets.logoSvg = `/media/${logoFilename}`;
|
|
console.log(`✅ SVG logo detected: ${logoFilename}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to get site_icon
|
|
if (settings.site_icon) {
|
|
console.log(`📥 Found site_icon ID: ${settings.site_icon}`);
|
|
assets.siteIconId = settings.site_icon;
|
|
const iconMedia = await fetchMedia(settings.site_icon);
|
|
if (iconMedia && iconMedia.source_url) {
|
|
// Download as favicon.ico
|
|
const faviconFilename = 'favicon.ico';
|
|
await downloadFavicon(iconMedia.source_url, faviconFilename);
|
|
assets.favicon = `/favicon.ico`;
|
|
console.log(`✅ Favicon downloaded: ${faviconFilename}`);
|
|
|
|
// Also create apple-touch-icon.png (same file, different name)
|
|
const appleTouchFilename = 'apple-touch-icon.png';
|
|
await downloadFavicon(iconMedia.source_url, appleTouchFilename);
|
|
assets.appleTouchIcon = `/apple-touch-icon.png`;
|
|
console.log(`✅ Apple touch icon downloaded: ${appleTouchFilename}`);
|
|
}
|
|
}
|
|
|
|
// WP CLI Equivalent: wp media list --search=logo --format=json
|
|
console.log('🔍 WP CLI Equivalent: Searching for logo media...');
|
|
if (!assets.logo) {
|
|
const allMedia = await fetchWithPagination('media', { per_page: 100 });
|
|
const logoCandidates = allMedia.filter(m => {
|
|
const title = m.title?.rendered?.toLowerCase() || '';
|
|
const slug = m.slug?.toLowerCase() || '';
|
|
const url = m.source_url?.toLowerCase() || '';
|
|
return title.includes('logo') || slug.includes('logo') || url.includes('logo');
|
|
});
|
|
|
|
if (logoCandidates.length > 0) {
|
|
const logoMedia = logoCandidates[0];
|
|
const ext = path.extname(logoMedia.source_url);
|
|
const logoFilename = `logo${ext}`;
|
|
await downloadMedia(logoMedia.source_url, logoFilename);
|
|
assets.logo = `/media/${logoFilename}`;
|
|
|
|
if (logoMedia.mime_type === 'image/svg+xml' || ext === '.svg') {
|
|
assets.logoSvg = `/media/${logoFilename}`;
|
|
console.log(`✅ SVG logo found and downloaded: ${logoFilename}`);
|
|
} else {
|
|
console.log(`✅ Logo found and downloaded: ${logoFilename}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// WP CLI Equivalent: wp media list --mime=image/svg+xml --format=json
|
|
console.log('🔍 WP CLI Equivalent: Searching for SVG images...');
|
|
const allMedia = await fetchWithPagination('media', { per_page: 200 });
|
|
const svgImages = allMedia.filter(m => m.mime_type === 'image/svg+xml');
|
|
|
|
if (svgImages.length > 0) {
|
|
console.log(`📥 Found ${svgImages.length} SVG images`);
|
|
for (const svg of svgImages) {
|
|
const filename = `svg-${svg.id}-${path.basename(svg.source_url)}`;
|
|
await downloadMedia(svg.source_url, filename);
|
|
console.log(`✅ SVG downloaded: ${filename}`);
|
|
}
|
|
}
|
|
|
|
// WP CLI Equivalent: wp postmeta list --post_type=any --meta_key~=_vc --format=json
|
|
console.log('🔍 WP CLI Equivalent: Searching for Salient/VC images...');
|
|
const salientImages = new Set();
|
|
|
|
// Search pages and posts for Visual Composer meta
|
|
const searchEndpoints = ['pages', 'posts'];
|
|
for (const endpoint of searchEndpoints) {
|
|
const items = await fetchWithPagination(endpoint, { per_page: 100 });
|
|
items.forEach(item => {
|
|
// Look for VC-related meta
|
|
if (item.meta) {
|
|
Object.keys(item.meta).forEach(key => {
|
|
if (key.includes('_vc') || key.includes('vc_') || key.includes('salient')) {
|
|
const metaValue = item.meta[key];
|
|
if (typeof metaValue === 'string') {
|
|
// Extract URLs from meta value
|
|
const urlMatches = metaValue.match(/https?:\/\/[^\s"']+/g);
|
|
if (urlMatches) {
|
|
urlMatches.forEach(url => salientImages.add(url));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Also check content for images
|
|
const content = item.content?.rendered || '';
|
|
const contentUrls = content.match(/https?:\/\/[^\s"']+\.(jpg|jpeg|png|webp|svg)/gi);
|
|
if (contentUrls) {
|
|
contentUrls.forEach(url => salientImages.add(url));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Download Salient/VC images
|
|
if (salientImages.size > 0) {
|
|
console.log(`📥 Found ${salientImages.size} Salient/VC images`);
|
|
const salientManifest = [];
|
|
|
|
for (const url of salientImages) {
|
|
try {
|
|
const filename = `salient-${Date.now()}-${path.basename(url)}`;
|
|
await downloadMedia(url, filename);
|
|
salientManifest.push({
|
|
originalUrl: url,
|
|
localPath: `/media/${filename}`,
|
|
filename: filename
|
|
});
|
|
console.log(`✅ Salient image downloaded: ${filename}`);
|
|
} catch (err) {
|
|
console.warn(`⚠️ Failed to download Salient image ${url}:`, err.message);
|
|
}
|
|
}
|
|
|
|
// Save Salient images manifest
|
|
fs.writeFileSync(
|
|
path.join(OUTPUT_DIR, 'salient-images.json'),
|
|
JSON.stringify(salientManifest, null, 2)
|
|
);
|
|
}
|
|
|
|
// If no favicon found, try to download from common locations
|
|
if (!assets.favicon) {
|
|
console.log('⚠️ No favicon found in settings, trying common locations...');
|
|
const faviconUrls = [
|
|
`${BASE_URL}/favicon.ico`,
|
|
`${BASE_URL}/wp-content/uploads/favicon.ico`
|
|
];
|
|
|
|
for (const url of faviconUrls) {
|
|
try {
|
|
await downloadFavicon(url, 'favicon.ico');
|
|
assets.favicon = '/favicon.ico';
|
|
console.log(`✅ Favicon downloaded from: ${url}`);
|
|
|
|
// Also create apple-touch-icon
|
|
await downloadFavicon(url, 'apple-touch-icon.png');
|
|
assets.appleTouchIcon = '/apple-touch-icon.png';
|
|
break;
|
|
} catch (e) {
|
|
// Continue to next URL
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save asset manifest
|
|
fs.writeFileSync(
|
|
path.join(OUTPUT_DIR, 'assets.json'),
|
|
JSON.stringify(assets, null, 2)
|
|
);
|
|
|
|
console.log('✅ Logo and favicon export complete');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error exporting logo/favicon:', error.message);
|
|
}
|
|
|
|
return assets;
|
|
}
|
|
|
|
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 exportWPCliPostmeta() {
|
|
console.log('\n📊 EXPORTING WP CLI POSTMETA (VC/Salient)');
|
|
|
|
const vcMeta = [];
|
|
|
|
try {
|
|
// Get all pages and posts
|
|
const pages = await fetchWithPagination('pages', { status: 'publish', per_page: 100 });
|
|
const posts = await fetchWithPagination('posts', { status: 'publish', per_page: 100 });
|
|
|
|
const allItems = [...pages, ...posts];
|
|
|
|
console.log(`🔍 Scanning ${allItems.length} items for VC/Salient meta...`);
|
|
|
|
allItems.forEach(item => {
|
|
if (item.meta) {
|
|
const vcKeys = Object.keys(item.meta).filter(key =>
|
|
key.includes('_vc') || key.includes('vc_') || key.includes('salient') || key.includes('wpb_')
|
|
);
|
|
|
|
if (vcKeys.length > 0) {
|
|
vcKeys.forEach(key => {
|
|
const value = item.meta[key];
|
|
vcMeta.push({
|
|
post_id: item.id,
|
|
post_type: item.type || 'page',
|
|
post_slug: item.slug,
|
|
meta_key: key,
|
|
meta_value: typeof value === 'string' ? value.substring(0, 200) : JSON.stringify(value),
|
|
full_value: value
|
|
});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Save VC postmeta
|
|
fs.writeFileSync(
|
|
path.join(OUTPUT_DIR, 'vc-postmeta.json'),
|
|
JSON.stringify(vcMeta, null, 2)
|
|
);
|
|
|
|
console.log(`✅ VC/Salient postmeta: ${vcMeta.length} entries found`);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error exporting postmeta:', error.message);
|
|
}
|
|
|
|
return vcMeta;
|
|
}
|
|
|
|
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 (WP CLI Enhanced)');
|
|
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();
|
|
|
|
// Step 2: WP CLI Enhanced exports
|
|
await exportWPCliPostmeta();
|
|
await exportMedia();
|
|
await exportLogoAndFavicon();
|
|
|
|
// Step 3: 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(`🎨 Logo/Favicon: public/`);
|
|
console.log('');
|
|
console.log('WP CLI Features:');
|
|
console.log('✓ SVG logo detection and download');
|
|
console.log('✓ All SVG images exported');
|
|
console.log('✓ Salient/VC postmeta extracted');
|
|
console.log('✓ All media downloaded locally');
|
|
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,
|
|
exportLogoAndFavicon,
|
|
exportWPCliPostmeta,
|
|
generateTranslationMapping,
|
|
generateRedirects
|
|
}; |