- Update data-fetching functions to include language and translation request parameters for improved internationalization. - Modify caching logic in Content and GrowTentKonfigurator components to utilize language-aware cache keys. - Ensure ProductDetailPage retrieves cached data based on the current language, enhancing user experience across different locales. - Integrate language handling in various components to maintain consistency in data management and rendering.
265 lines
10 KiB
JavaScript
265 lines
10 KiB
JavaScript
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(/<script[^>]*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 += `<link rel="preload" href="${cssFile}" as="style" onload="this.onload=null;this.rel='stylesheet'">`;
|
|
additionalTags += `<noscript><link rel="stylesheet" href="${cssFile}"></noscript>`;
|
|
if (!suppressLogs) console.log(` ⚠️ CSS file not found for inlining: ${cssPath}, using async loading`);
|
|
}
|
|
} catch (error) {
|
|
// Fallback to external loading if reading fails
|
|
additionalTags += `<link rel="preload" href="${cssFile}" as="style" onload="this.onload=null;this.rel='stylesheet'">`;
|
|
additionalTags += `<noscript><link rel="stylesheet" href="${cssFile}"></noscript>`;
|
|
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 += `<script src="${jsFile}"></script>`;
|
|
}
|
|
});
|
|
} 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 += `<link rel="stylesheet" href="/prerender.css">`;
|
|
}
|
|
} catch (error) {
|
|
// Fallback to external loading
|
|
additionalTags += `<link rel="stylesheet" href="/prerender.css">`;
|
|
}
|
|
}
|
|
|
|
// Create script to save prerendered content to window object for fallback use
|
|
const prerenderFallbackScript = `
|
|
<script>
|
|
// Save prerendered content to window object for SocketProvider fallback
|
|
window.__PRERENDER_FALLBACK__ = {
|
|
path: '${location}',
|
|
content: ${JSON.stringify(renderedMarkup)},
|
|
timestamp: ${Date.now()}
|
|
};
|
|
|
|
// DEBUG: Multiple alerts throughout the loading process
|
|
// Debug alerts removed
|
|
|
|
|
|
</script>
|
|
`;
|
|
|
|
// @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 = `
|
|
<script>
|
|
// Populate window.categoryCache with static category tree only
|
|
window.categoryCache = ${staticCacheData};
|
|
</script>
|
|
`;
|
|
}
|
|
|
|
// 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 = `
|
|
<script>
|
|
// Populate window.productDetailCache with complete product data for SPA hydration
|
|
if (!window.productDetailCache) {
|
|
window.productDetailCache = {};
|
|
}
|
|
window.productDetailCache['${cacheKey}'] = ${productDetailCacheData};
|
|
</script>
|
|
`;
|
|
}
|
|
|
|
// Combine all CSS (global + inlined) into a single optimized style tag
|
|
const combinedCss = globalCss + (inlinedCss ? '\n' + inlinedCss : '');
|
|
const combinedCssTag = combinedCss ? `<style type="text/css">${combinedCss}</style>` : '';
|
|
|
|
// Add resource hints for better performance
|
|
const resourceHints = `
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
`;
|
|
|
|
template = template.replace(
|
|
"</head>",
|
|
`${resourceHints}${combinedCssTag}${additionalTags}${metaTags}${prerenderFallbackScript}${productCacheScript}${productDetailCacheScript}</head>`
|
|
);
|
|
|
|
const rootDivRegex = /<div id="root"[\s\S]*?>[\s\S]*?<\/div>/;
|
|
const replacementHtml = `<div id="root">${renderedMarkup}</div>`;
|
|
|
|
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("<body>", `<body>${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`);
|
|
console.log(` - CSS rules: ${Object.keys(cache.inserted).length}`);
|
|
console.log(` - Total inlined CSS: ${Math.round(combinedCss.length / 1024)}KB`);
|
|
console.log(` - Render-blocking CSS eliminated: ${inlinedCss ? 'YES' : 'NO'}`);
|
|
console.log(` - Fallback content saved to window.__PRERENDER_FALLBACK__`);
|
|
if (productDetailCacheScript) {
|
|
console.log(` - Product detail cache populated for SPA hydration`);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
module.exports = {
|
|
renderPage,
|
|
};
|