Files
reactShop/prerender.cjs

901 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require("@babel/register")({
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-react",
],
extensions: [".js", ".jsx"],
ignore: [/node_modules/],
});
// Minimal globals for socket.io-client only - no JSDOM to avoid interference
global.window = {}; // Minimal window object for productCache
global.navigator = { userAgent: "node.js" };
// Use Node.js URL constructor for React Router compatibility
global.URL = require("url").URL;
global.Blob = class MockBlob {
constructor(data, options) {
this.data = data;
this.type = options?.type || "";
}
};
class CategoryService {
constructor() {
this.get = this.get.bind(this);
}
getSync(categoryId, language = "de") {
const cacheKey = `${categoryId}_${language}`;
return null;
}
async get(categoryId, language = "de") {
const cacheKey = `${categoryId}_${language}`;
return null;
}
}
global.window.categoryService = new CategoryService();
// Import modules
const fs = require("fs");
const path = require("path");
const React = require("react");
const io = require("socket.io-client");
const os = require("os");
const { Worker, isMainThread, parentPort, workerData } = require("worker_threads");
// Initialize i18n for prerendering with German as default
const i18n = require("i18next");
const { initReactI18next } = require("react-i18next");
// Import all translation files
const translationDE = require("./src/i18n/locales/de/translation.js").default;
const translationEN = require("./src/i18n/locales/en/translation.js").default;
const translationAR = require("./src/i18n/locales/ar/translation.js").default;
const translationBG = require("./src/i18n/locales/bg/translation.js").default;
const translationCS = require("./src/i18n/locales/cs/translation.js").default;
const translationEL = require("./src/i18n/locales/el/translation.js").default;
const translationES = require("./src/i18n/locales/es/translation.js").default;
const translationFR = require("./src/i18n/locales/fr/translation.js").default;
const translationHR = require("./src/i18n/locales/hr/translation.js").default;
const translationHU = require("./src/i18n/locales/hu/translation.js").default;
const translationIT = require("./src/i18n/locales/it/translation.js").default;
const translationPL = require("./src/i18n/locales/pl/translation.js").default;
const translationRO = require("./src/i18n/locales/ro/translation.js").default;
const translationRU = require("./src/i18n/locales/ru/translation.js").default;
const translationSK = require("./src/i18n/locales/sk/translation.js").default;
const translationSL = require("./src/i18n/locales/sl/translation.js").default;
const translationSR = require("./src/i18n/locales/sr/translation.js").default;
const translationSV = require("./src/i18n/locales/sv/translation.js").default;
const translationTR = require("./src/i18n/locales/tr/translation.js").default;
const translationUK = require("./src/i18n/locales/uk/translation.js").default;
const translationZH = require("./src/i18n/locales/zh/translation.js").default;
// Initialize i18n for prerendering
i18n
.use(initReactI18next)
.init({
resources: {
de: { translation: translationDE },
en: { translation: translationEN },
ar: { translation: translationAR },
bg: { translation: translationBG },
cs: { translation: translationCS },
el: { translation: translationEL },
es: { translation: translationES },
fr: { translation: translationFR },
hr: { translation: translationHR },
hu: { translation: translationHU },
it: { translation: translationIT },
pl: { translation: translationPL },
ro: { translation: translationRO },
ru: { translation: translationRU },
sk: { translation: translationSK },
sl: { translation: translationSL },
sr: { translation: translationSR },
sv: { translation: translationSV },
tr: { translation: translationTR },
uk: { translation: translationUK },
zh: { translation: translationZH }
},
lng: 'de', // Default to German for prerendering
fallbackLng: 'de',
debug: false,
interpolation: {
escapeValue: false
},
react: {
useSuspense: false
}
});
// Make i18n available globally for components
global.i18n = i18n;
// Import split modules
const config = require("./prerender/config.cjs");
// Import shop config - using require with Babel transpilation
const shopConfig = require("./src/config.js").default;
const { renderPage } = require("./prerender/renderer.cjs");
const {
collectAllCategories,
} = require("./prerender/utils.cjs");
const {
generateProductMetaTags,
generateProductJsonLd,
generateCategoryJsonLd,
generateHomepageMetaTags,
generateHomepageJsonLd,
generateSitemapJsonLd,
generateKonfiguratorMetaTags,
generateXmlSitemap,
generateRobotsTxt,
generateProductsXml,
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
generateCategoryProductList,
} = require("./prerender/seo.cjs");
const {
fetchCategoryProducts,
fetchProductDetails,
saveProductImages,
saveCategoryImages,
} = require("./prerender/data-fetching.cjs");
// Import components
const PrerenderCategory = require("./src/PrerenderCategory.js").default;
const PrerenderProduct = require("./src/PrerenderProduct.js").default;
const PrerenderKonfigurator = require("./src/PrerenderKonfigurator.js").default;
const PrerenderProfile = require("./src/PrerenderProfile.js").default;
// Import static page components
const Datenschutz = require("./src/pages/Datenschutz.js").default;
const Impressum = require("./src/pages/Impressum.js").default;
const Batteriegesetzhinweise =
require("./src/pages/Batteriegesetzhinweise.js").default;
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.js").default;
const PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
const PrerenderCategoriesPage = require("./src/PrerenderCategoriesPage.js").default;
const AGB = require("./src/pages/AGB.js").default;
const NotFound404 = require("./src/pages/NotFound404.js").default;
// Worker function for parallel product rendering
const renderProductWorker = async (productSeoNames, workerId, progressCallback, categoryMap = {}) => {
const socketUrl = "http://127.0.0.1:9303";
const workerSocket = io(socketUrl, {
path: "/socket.io/",
transports: ["websocket"],
reconnection: false,
timeout: 10000,
});
return new Promise((resolve) => {
let processedCount = 0;
let successCount = 0;
const results = [];
const processNextProduct = async () => {
if (processedCount >= productSeoNames.length) {
workerSocket.disconnect();
resolve({ successCount, results, workerId });
return;
}
const productSeoName = productSeoNames[processedCount];
processedCount++;
try {
const productDetails = await fetchProductDetails(workerSocket, productSeoName);
const actualSeoName = productDetails.product.seoName || productSeoName;
const productComponent = React.createElement(PrerenderProduct, {
productData: productDetails,
t: global.i18n.t.bind(global.i18n),
});
const filename = `Artikel/${actualSeoName}`;
const location = `/Artikel/${actualSeoName}`;
const description = `Product "${productDetails.product.name}" (seoName: ${productSeoName})`;
const metaTags = generateProductMetaTags({
...productDetails.product,
seoName: actualSeoName,
}, shopConfig.baseUrl, shopConfig);
// Get category info from categoryMap if available
const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null;
const jsonLdScript = generateProductJsonLd({
...productDetails.product,
seoName: actualSeoName,
}, shopConfig.baseUrl, shopConfig, categoryInfo);
const combinedMetaTags = metaTags + "\n" + jsonLdScript;
const success = renderPage(
productComponent,
location,
filename,
description,
combinedMetaTags,
true,
config,
true, // Suppress logs during parallel rendering to avoid interfering with progress bar
productDetails // Pass product data for cache population
);
if (success) {
successCount++;
}
const result = {
productSeoName,
productName: productDetails.product.name,
success,
workerId
};
results.push(result);
// Call progress callback if provided
if (progressCallback) {
progressCallback(result);
}
// Small delay to avoid overwhelming the server
setTimeout(processNextProduct, 25);
} catch (error) {
const result = {
productSeoName,
productName: productSeoName,
success: false,
error: error.message,
workerId
};
results.push(result);
// Call progress callback if provided
if (progressCallback) {
progressCallback(result);
}
setTimeout(processNextProduct, 25);
}
};
workerSocket.on("connect", () => {
processNextProduct();
});
workerSocket.on("connect_error", (err) => {
console.error(`Worker ${workerId} socket connection error:`, err);
resolve({ successCount: 0, results: [], workerId });
});
});
};
// Function to render products in parallel
const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProducts, categoryMap = {}) => {
// Shared progress tracking
let completedProducts = 0;
let totalSuccessCount = 0;
let lastProductName = '';
const progressResults = [];
const workerCounts = new Array(maxWorkers).fill(0); // Track per-worker progress
const workerSuccess = new Array(maxWorkers).fill(0); // Track per-worker success count
// Helper function to display progress bar with worker stats
const updateProgressBar = (current, total, productName = '') => {
const percentage = Math.round((current / total) * 100);
const barLength = 30;
const filledLength = Math.round((barLength * current) / total);
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
// @note Single line progress update to prevent flickering
const truncatedName = productName ? ` - ${productName.substring(0, 25)}${productName.length > 25 ? '...' : ''}` : '';
// Build worker stats on one line
let workerStats = '';
for (let i = 0; i < Math.min(maxWorkers, 8); i++) { // Limit to 8 workers to fit on screen
workerStats += `W${i + 1}:${workerCounts[i]}/${workerSuccess[i]} `;
}
// Single line update without complex cursor movements
process.stdout.write(`\r [${bar}] ${percentage}% (${current}/${total})${truncatedName}\n ${workerStats}${current < total ? '\x1b[1A' : '\n'}`);
};
// Split products among workers
const productsPerWorker = Math.ceil(allProductsArray.length / maxWorkers);
const workerPromises = [];
// Initial progress bar
updateProgressBar(0, totalProducts);
for (let i = 0; i < maxWorkers; i++) {
const start = i * productsPerWorker;
const end = Math.min(start + productsPerWorker, allProductsArray.length);
const productsForWorker = allProductsArray.slice(start, end);
if (productsForWorker.length > 0) {
const promise = renderProductWorker(productsForWorker, i + 1, (result) => {
// Progress callback - called each time a product is completed
completedProducts++;
progressResults.push(result);
lastProductName = result.productName;
// Update per-worker counters
const workerIndex = result.workerId - 1; // Convert to 0-based index
workerCounts[workerIndex]++;
if (result.success) {
totalSuccessCount++;
workerSuccess[workerIndex]++;
} else if (result.error) {
// Don't log errors immediately to avoid interfering with progress bar
// Errors will be shown after completion
}
// Update progress bar with worker stats
updateProgressBar(completedProducts, totalProducts, lastProductName);
}, categoryMap);
workerPromises.push(promise);
}
}
try {
// Wait for all workers to complete
await Promise.all(workerPromises);
// Ensure final progress update
updateProgressBar(totalProducts, totalProducts, lastProductName);
// Show any errors that occurred
const errorResults = progressResults.filter(r => !r.success && r.error);
if (errorResults.length > 0) {
console.log(`\n${errorResults.length} products failed to render:`);
errorResults.forEach(result => {
console.log(` - ${result.productSeoName}: ${result.error}`);
});
}
return totalSuccessCount;
} catch (error) {
console.error('Error in parallel rendering:', error);
return totalSuccessCount; // Return what we managed to complete
}
};
const renderApp = async (categoryData, socket) => {
if (categoryData) {
global.window.categoryCache = {
"209_de": categoryData,
};
// @note Make cache available to components during rendering
global.categoryCache = global.window.categoryCache;
} else {
global.window.categoryCache = {};
global.categoryCache = {};
}
// Helper to call renderPage with config
const render = (
component,
location,
filename,
description,
metaTags = "",
needsRouter = false
) => {
return renderPage(
component,
location,
filename,
description,
metaTags,
needsRouter,
config
);
};
console.log("🏠 Rendering home page...");
const PrerenderHome = require("./src/PrerenderHome.js").default;
const homeComponent = React.createElement(PrerenderHome, null);
const homeFilename = config.isProduction
? "index.html"
: "index.prerender.html";
const homeMetaTags = generateHomepageMetaTags(shopConfig.baseUrl, shopConfig);
const homepageCategories = categoryData ? collectAllCategories(categoryData) : [];
const homeJsonLd = generateHomepageJsonLd(shopConfig.baseUrl, shopConfig, homepageCategories);
const combinedHomeMeta = homeMetaTags + "\n" + homeJsonLd;
const homeSuccess = render(
homeComponent,
"/",
homeFilename,
"Home page",
combinedHomeMeta,
true
);
if (!homeSuccess) {
process.exit(1);
}
// Copy index.html to resetPassword (no file extension) for SPA routing
if (config.isProduction) {
const indexPath = path.resolve(__dirname, config.outputDir, "index.html");
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
fs.copyFileSync(indexPath, resetPasswordPath);
console.log(`✅ Copied index.html to ${resetPasswordPath}`);
}
// Render static pages
console.log("\n📄 Rendering static pages...");
const staticPages = [
{
component: Datenschutz,
path: "/datenschutz",
filename: "datenschutz",
description: "Datenschutz page",
},
{
component: Impressum,
path: "/impressum",
filename: "impressum",
description: "Impressum page",
},
{
component: Batteriegesetzhinweise,
path: "/batteriegesetzhinweise",
filename: "batteriegesetzhinweise",
description: "Batteriegesetzhinweise page",
},
{
component: Widerrufsrecht,
path: "/widerrufsrecht",
filename: "widerrufsrecht",
description: "Widerrufsrecht page",
},
{
component: PrerenderSitemap,
path: "/sitemap",
filename: "sitemap",
description: "Sitemap page",
needsCategoryData: true,
},
{
component: PrerenderCategoriesPage,
path: "/Kategorien",
filename: "Kategorien",
description: "Categories page",
needsCategoryData: true,
},
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
{
component: PrerenderKonfigurator,
path: "/Konfigurator",
filename: "Konfigurator",
description: "Growbox Konfigurator page",
},
{
component: PrerenderProfile,
path: "/profile",
filename: "profile",
description: "Profile page",
},
];
let staticPagesRendered = 0;
for (const page of staticPages) {
// Pass category data as props if needed
const pageProps = page.needsCategoryData ? { categoryData } : null;
const pageComponent = React.createElement(page.component, pageProps);
let metaTags = "";
// Special handling for Sitemap page to include category data
if (page.filename === "sitemap" && categoryData) {
const sitemapCategories = collectAllCategories(categoryData);
metaTags = generateSitemapJsonLd(sitemapCategories, shopConfig.baseUrl, shopConfig);
}
// Special handling for Konfigurator page to include SEO tags
if (page.filename === "Konfigurator") {
const konfiguratorMetaTags = generateKonfiguratorMetaTags(shopConfig.baseUrl, shopConfig);
metaTags = konfiguratorMetaTags;
}
const success = render(
pageComponent,
page.path,
page.filename,
page.description,
metaTags,
true
);
if (success) {
staticPagesRendered++;
}
}
console.log(
`✅ Successfully rendered ${staticPagesRendered}/${staticPages.length} static pages!`
);
// Collect all products for product page generation
const allProducts = new Set();
const allProductsData = []; // @note Store full product data for products.xml generation
// Generate category pages if we have category data
if (categoryData && socket) {
console.log("\n📂 Rendering category pages with product data...");
const allCategories = collectAllCategories(categoryData);
console.log(`Found ${allCategories.length} categories to render`);
// First, collect category images for all categories
console.log("\n📂 Collecting category images...");
await saveCategoryImages(socket, allCategories, config.outputDir);
let categoryPagesRendered = 0;
let categoriesWithProducts = 0;
const allCategoriesPlusNeu = [...allCategories, {
id: "neu",
name: "Neuheiten",
seoName: "neu",
parentId: 209
}];
for (const category of allCategoriesPlusNeu) {
// Skip categories without seoName
if (!category.seoName) {
console.log(
`⚠️ Skipping category "${category.name}" (ID: ${category.id}) - no seoName`
);
continue;
}
try {
console.log(
`\n🔍 Fetching products for category "${category.name}" (ID: ${category.id})...`
);
let productData = null;
try {
productData = await fetchCategoryProducts(socket, category.id);
console.log(
` ✅ Found ${productData.products ? productData.products.length : 0
} products`
);
if (productData.products && productData.products.length > 0) {
categoriesWithProducts++;
// Collect products for individual page generation
productData.products.forEach((product) => {
if (product.seoName) {
allProducts.add(product.seoName);
// @note Store full product data for products.xml generation with category ID
allProductsData.push({
...product,
seoName: product.seoName,
categoryId: category.id // Add the category ID for Google Shopping category mapping
});
}
});
// Fetch and save product images
await saveProductImages(
socket,
productData.products,
category.name,
config.outputDir
);
// Don't accumulate data in global cache - just use the data directly for this page
// The global cache should only contain the static category tree
}
} catch (productError) {
console.log(` ⚠️ No products found: ${productError.message}`);
}
const categoryComponent = React.createElement(PrerenderCategory, {
categoryId: category.id,
categoryName: category.name,
categorySeoName: category.seoName,
productData: productData,
});
const filename = `Kategorie/${category.seoName}`;
const location = `/Kategorie/${category.seoName}`;
const description = `Category "${category.name}" (ID: ${category.id})`;
const categoryJsonLd = generateCategoryJsonLd(
category,
productData?.products || [],
shopConfig.baseUrl,
shopConfig
);
const success = render(
categoryComponent,
location,
filename,
description,
categoryJsonLd,
true
);
if (success) {
categoryPagesRendered++;
}
// Small delay to avoid overwhelming the server
await new Promise((resolve) => setTimeout(resolve, 100));
} catch (error) {
console.error(
`❌ Failed to render category ${category.id} (${category.name}):`,
error
);
}
}
console.log(
`\n🎉 Successfully rendered ${categoryPagesRendered} category pages!`
);
console.log(`📦 ${categoriesWithProducts} categories had product data`);
// Generate individual product pages
if (allProducts.size > 0) {
const totalProducts = allProducts.size;
const numCPUs = os.cpus().length;
const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server
// Create category map for breadcrumbs
const categoryMap = {};
allCategories.forEach(category => {
categoryMap[category.id] = {
name: category.name,
seoName: category.seoName
};
});
console.log(
`\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...`
);
const productPagesRendered = await renderProductsInParallel(
Array.from(allProducts),
maxWorkers,
totalProducts,
categoryMap
);
console.log(
`🎉 Successfully rendered ${productPagesRendered}/${totalProducts} product pages!`
);
}
} else {
console.log(
"⚠️ No category data or socket available - skipping category page generation"
);
}
// No longer writing combined CSS file - each page has its own embedded CSS
// Generate XML sitemap with all rendered pages
console.log("\n🗺 Generating XML sitemap...");
const allCategories = categoryData ? collectAllCategories(categoryData) : [];
const allProductsArray = Array.from(allProducts);
const xmlSitemap = generateXmlSitemap(allCategories, allProductsArray, shopConfig.baseUrl);
const sitemapPath = path.resolve(__dirname, config.outputDir, "sitemap.xml");
fs.writeFileSync(sitemapPath, xmlSitemap);
console.log(`✅ XML sitemap generated: ${sitemapPath}`);
console.log(` - Homepage: 1 URL`);
console.log(` - Static pages: 6 URLs`);
console.log(` - Category pages: ${allCategories.length} URLs`);
console.log(` - Product pages: ${allProductsArray.length} URLs`);
console.log(
` - Total URLs: ${1 + 6 + allCategories.length + allProductsArray.length}`
);
// Generate robots.txt
console.log("\n🤖 Generating robots.txt...");
const robotsTxtContent = generateRobotsTxt(shopConfig.baseUrl);
const robotsTxtPath = path.resolve(__dirname, config.outputDir, "robots.txt");
fs.writeFileSync(robotsTxtPath, robotsTxtContent);
console.log(`✅ robots.txt generated: ${robotsTxtPath}`);
console.log(` - Allows all crawlers`);
console.log(` - References sitemap.xml`);
console.log(` - Includes crawl-delay directive`);
// Generate products.xml (Google Shopping feed) in parallel to sitemap.xml
if (allProductsData.length > 0) {
console.log("\n🛒 Generating products.xml (Google Shopping feed)...");
try {
const productsXml = generateProductsXml(allProductsData, shopConfig.baseUrl, shopConfig);
const productsXmlPath = path.resolve(__dirname, config.outputDir, "products.xml");
// Write with explicit UTF-8 encoding
fs.writeFileSync(productsXmlPath, productsXml, { encoding: 'utf8' });
console.log(`✅ products.xml generated: ${productsXmlPath}`);
console.log(` - Products included: ${allProductsData.length}`);
console.log(` - Format: Google Shopping RSS 2.0 feed`);
console.log(` - Encoding: UTF-8`);
console.log(` - Includes: title, description, price, availability, images`);
// Verify the file is valid UTF-8
try {
const verification = fs.readFileSync(productsXmlPath, 'utf8');
console.log(` - File verification: ✅ Valid UTF-8 (${Math.round(verification.length / 1024)}KB)`);
} catch (verifyError) {
console.log(` - File verification: ⚠️ ${verifyError.message}`);
}
// Validate XML against Google Shopping schema
try {
const ProductsXmlValidator = require('./scripts/validate-products-xml.cjs');
const validator = new ProductsXmlValidator(productsXmlPath);
const validationResults = await validator.validate();
if (validationResults.valid) {
console.log(` - Schema validation: ✅ Valid Google Shopping RSS 2.0`);
} else {
console.log(` - Schema validation: ⚠️ ${validationResults.summary.errorCount} errors, ${validationResults.summary.warningCount} warnings`);
// Show first few errors for quick debugging
if (validationResults.errors.length > 0) {
console.log(` - First error: ${validationResults.errors[0].message}`);
}
}
} catch (validationError) {
console.log(` - Schema validation: ⚠️ Validation failed: ${validationError.message}`);
}
} catch (error) {
console.error(`❌ Error generating products.xml: ${error.message}`);
console.log("\n⚠ Skipping products.xml generation due to errors");
}
} else {
console.log("\n⚠ No product data available - skipping products.xml generation");
}
// Generate llms.txt (LLM-friendly markdown sitemap) and category-specific files
console.log("\n🤖 Generating LLM sitemap files...");
try {
// Generate main llms.txt overview file
const llmsTxt = generateLlmsTxt(allCategories, allProductsData, shopConfig.baseUrl, shopConfig);
const llmsTxtPath = path.resolve(__dirname, config.outputDir, "llms.txt");
fs.writeFileSync(llmsTxtPath, llmsTxt, { encoding: 'utf8' });
console.log(`✅ Main llms.txt generated: ${llmsTxtPath}`);
console.log(` - Static pages: 8 pages`);
console.log(` - Categories: ${allCategories.length} with links to detailed files`);
console.log(` - File size: ${Math.round(llmsTxt.length / 1024)}KB`);
// Group products by category for category-specific files
const productsByCategory = {};
allProductsData.forEach((product) => {
const categoryId = product.categoryId || 'uncategorized';
if (!productsByCategory[categoryId]) {
productsByCategory[categoryId] = [];
}
productsByCategory[categoryId].push(product);
});
// Generate category-specific LLM files with pagination
let categoryFilesGenerated = 0;
let totalCategoryProducts = 0;
let totalPaginatedFiles = 0;
for (const category of allCategories) {
if (category.seoName) {
const categoryProducts = productsByCategory[category.id] || [];
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
// Generate all paginated files for this category
const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig);
// Write each paginated file
for (const page of categoryPages) {
const pagePath = path.resolve(__dirname, config.outputDir, page.fileName);
fs.writeFileSync(pagePath, page.content, { encoding: 'utf8' });
totalPaginatedFiles++;
}
// Generate and write the product list file for this category
const productList = generateCategoryProductList(category, categoryProducts);
const listPath = path.resolve(__dirname, config.outputDir, productList.fileName);
fs.writeFileSync(listPath, productList.content, { encoding: 'utf8' });
const pageCount = categoryPages.length;
const totalSize = categoryPages.reduce((sum, page) => sum + page.content.length, 0);
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
console.log(` 📋 ${productList.fileName} - ${productList.productCount} products (${Math.round(productList.content.length / 1024)}KB)`);
categoryFilesGenerated++;
totalCategoryProducts += categoryProducts.length;
}
}
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
try {
const verification = fs.readFileSync(llmsTxtPath, 'utf8');
console.log(` - File verification: ✅ All files valid UTF-8`);
} catch (verifyError) {
console.log(` - File verification: ⚠️ ${verifyError.message}`);
}
} catch (error) {
console.error(`❌ Error generating LLM sitemap files: ${error.message}`);
console.log("\n⚠ Skipping LLM sitemap generation due to errors");
}
};
const fetchCategoryDataAndRender = () => {
const socketUrl = "http://127.0.0.1:9303";
console.log(`Connecting to socket at ${socketUrl} to fetch categories...`);
const timeout = setTimeout(() => {
console.error(
"Error: Prerender script timed out after 15 seconds. Check backend connectivity."
);
process.exit(1);
}, 15000);
const socket = io(socketUrl, {
path: "/socket.io/",
transports: ["websocket"],
reconnection: false,
timeout: 10000,
});
socket.on("connect", () => {
console.log('Socket connected. Emitting "categoryList"...');
socket.emit("categoryList", { categoryId: 209 }, async (response) => {
clearTimeout(timeout);
if (response && response.categoryTree) {
console.log("Successfully fetched category data.");
await renderApp(response.categoryTree, socket);
} else {
console.error("Error: Invalid category data received.", response);
await renderApp(null, socket);
}
socket.disconnect();
});
});
socket.on("connect_error", async (err) => {
clearTimeout(timeout);
console.error("Socket connection error:", err);
await renderApp(null, null);
socket.disconnect();
});
socket.on("error", async (err) => {
clearTimeout(timeout);
console.error("Socket error:", err);
await renderApp(null, null);
socket.disconnect();
});
socket.on("disconnect", (reason) => {
console.log(`Socket disconnected: ${reason}`);
clearTimeout(timeout);
});
};
fetchCategoryDataAndRender();