const fs = require("fs"); const path = require("path"); const React = require("react"); const ReactDOMServer = require("react-dom/server"); const { StaticRouter } = require("react-router"); const { CacheProvider } = require("@emotion/react"); const { ThemeProvider } = require("@mui/material/styles"); const createEmotionCache = require("../createEmotionCache.js").default; const theme = require("../src/theme.js").default; const createEmotionServer = require("@emotion/server/create-instance").default; const renderPage = ( component, location, filename, description, metaTags = "", needsRouter = false, config, suppressLogs = false, productData = null ) => { const { isProduction, outputDir, globalCss, globalCssCollection, webpackEntrypoints, } = config; const { optimizeCss } = require("./utils.cjs"); // @note Set prerender fallback flag in global environment for CategoryBox during SSR if (typeof global !== "undefined" && global.window) { global.window.__PRERENDER_FALLBACK__ = { path: location, timestamp: Date.now() }; } // Create fresh Emotion cache for each page const cache = createEmotionCache(); const { extractCriticalToChunks } = createEmotionServer(cache); const wrappedComponent = needsRouter ? React.createElement(StaticRouter, { location: location }, component) : component; const pageElement = React.createElement( CacheProvider, { value: cache }, React.createElement(ThemeProvider, { theme: theme }, wrappedComponent) ); let renderedMarkup; let pageSpecificCss = ""; // Declare outside try block for broader scope try { renderedMarkup = ReactDOMServer.renderToString(pageElement); const emotionChunks = extractCriticalToChunks(renderedMarkup); // Collect CSS from this page for direct inlining (no global accumulation) if (emotionChunks.styles.length > 0) { emotionChunks.styles.forEach((style) => { if (style.css) { pageSpecificCss += style.css + "\n"; } }); if (!suppressLogs) console.log(` - CSS rules: ${emotionChunks.styles.length}`); } } catch (error) { console.error(`❌ Rendering failed for ${filename}:`, error); return false; } // Use appropriate template path based on mode // In production, use a clean template file, not the already-rendered index.html const templatePath = isProduction ? path.resolve(__dirname, "..", "dist", "index_template.html") : path.resolve(__dirname, "..", "public", "index.html"); let template = fs.readFileSync(templatePath, "utf8"); // Build CSS and JS tags with optimized CSS loading let additionalTags = ""; let inlinedCss = ""; if (isProduction) { // Check if scripts are already present in template to avoid duplication const existingScripts = template.match(/]*src="([^"]*)"[^>]*><\/script>/g) || []; const existingScriptSrcs = existingScripts .map((script) => { const match = script.match(/src="([^"]*)"/); return match ? match[1] : null; }) .filter(Boolean); // OPTIMIZATION: Inline critical CSS instead of loading externally // Read and inline webpack CSS files to eliminate render-blocking requests webpackEntrypoints.css.forEach((cssFile) => { if (!template.includes(`href="${cssFile}"`)) { try { const cssPath = path.resolve(__dirname, "..", "dist", cssFile.replace(/^\//, "")); if (fs.existsSync(cssPath)) { const cssContent = fs.readFileSync(cssPath, "utf8"); // Use advanced CSS optimization const optimizedCss = optimizeCss(cssContent); inlinedCss += optimizedCss; if (!suppressLogs) console.log(` ✅ Inlined CSS: ${cssFile} (${Math.round(optimizedCss.length / 1024)}KB)`); } else { // Fallback to external loading if file not found additionalTags += ``; additionalTags += ``; if (!suppressLogs) console.log(` ⚠️ CSS file not found for inlining: ${cssPath}, using async loading`); } } catch (error) { // Fallback to external loading if reading fails additionalTags += ``; additionalTags += ``; if (!suppressLogs) console.log(` ⚠️ Error reading CSS file ${cssFile}: ${error.message}, using async loading`); } } }); // Inline page-specific CSS directly (no shared prerender.css file) if (pageSpecificCss.trim()) { // Use advanced CSS optimization on page-specific CSS const optimizedPageCss = optimizeCss(pageSpecificCss); inlinedCss += optimizedPageCss; if (!suppressLogs) console.log(` ✅ Inlined page-specific CSS (${Math.round(optimizedPageCss.length / 1024)}KB)`); } // Add JavaScript files webpackEntrypoints.js.forEach((jsFile) => { if (!existingScriptSrcs.includes(jsFile)) { additionalTags += ``; } }); } else { // In development, try to inline prerender CSS as well try { const prerenderCssPath = path.resolve(__dirname, "..", outputDir, "prerender.css"); if (fs.existsSync(prerenderCssPath)) { const prerenderCssContent = fs.readFileSync(prerenderCssPath, "utf8"); const optimizedCss = optimizeCss(prerenderCssContent); inlinedCss += optimizedCss; if (!suppressLogs) console.log(` ✅ Inlined prerender CSS in development (${Math.round(optimizedCss.length / 1024)}KB)`); } else { // Fallback to external loading additionalTags += ``; } } catch (error) { // Fallback to external loading additionalTags += ``; } } // Create script to save prerendered content to window object for fallback use const prerenderFallbackScript = ` `; // @note Create script to populate window.productCache with ONLY the static category tree let productCacheScript = ''; if (typeof global !== "undefined" && global.window && global.window.categoryCache) { // Only include the static categoryTree_209, not any dynamic data that gets added during rendering const staticCache = {}; if (global.window.categoryCache["209_de"]) { staticCache["209_de"] = global.window.categoryCache["209_de"]; } const staticCacheData = JSON.stringify(staticCache); productCacheScript = ` `; } // Create script to populate window.productDetailCache for individual product pages let productDetailCacheScript = ''; if (productData && productData.product) { // Cache the entire response object (includes product, attributes, etc.) // Use language-aware cache key (prerender defaults to German) const productDetailCacheData = JSON.stringify(productData); const language = 'de'; // Prerender system caches German version const cacheKey = `product_${productData.product.seoName}_${language}`; productDetailCacheScript = ` `; } // Combine all CSS (global + inlined) into a single optimized style tag const combinedCss = globalCss + (inlinedCss ? '\n' + inlinedCss : ''); const combinedCssTag = combinedCss ? `` : ''; // Add resource hints for better performance const resourceHints = ` `; template = template.replace( "", `${resourceHints}${combinedCssTag}${additionalTags}${metaTags}${prerenderFallbackScript}${productCacheScript}${productDetailCacheScript}` ); const rootDivRegex = /
[\s\S]*?<\/div>/; const replacementHtml = `
${renderedMarkup}
`; let newHtml; if (rootDivRegex.test(template)) { if (!suppressLogs) console.log(` 📝 Root div found, replacing with ${renderedMarkup.length} chars of markup`); newHtml = template.replace(rootDivRegex, replacementHtml); } else { if (!suppressLogs) console.log(` ⚠️ No root div found, appending to body`); newHtml = template.replace("", `${replacementHtml}`); } const outputPath = path.resolve(__dirname, "..", outputDir, filename); // Ensure directory exists for nested paths const outputDirPath = path.dirname(outputPath); if (!fs.existsSync(outputDirPath)) { fs.mkdirSync(outputDirPath, { recursive: true }); } fs.writeFileSync(outputPath, newHtml); if (!suppressLogs) { console.log(`✅ ${description} prerendered to ${outputPath}`); console.log(` - Markup length: ${renderedMarkup.length} characters`); if (productDetailCacheScript) { console.log(` - Product detail cache populated for SPA hydration`); } } return true; }; module.exports = { renderPage, };