From 9e14827c91cfc36dd74e33fcc1e62ca92b4a9599 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 7 Jul 2025 02:12:19 +0200 Subject: [PATCH] Enhance 404 handling in webpack configuration with async middleware for prerendering. Updated NotFound404 page to improve user experience with localized messaging and responsive image styling. Added taxonomy ID mappings in feeds.cjs for better compliance and clarity in product categorization. --- .gitignore | 2 + prerender/seo/feeds.cjs | 176 ++++++++++++++++---------------- src/PrerenderNotFound.js | 92 +++++++++++++++++ src/pages/NotFound404.js | 9 +- webpack.config.js | 215 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 386 insertions(+), 108 deletions(-) create mode 100644 src/PrerenderNotFound.js diff --git a/.gitignore b/.gitignore index 5bb675e..f69d527 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,8 @@ yarn-error.log* # Local configuration src/config.local.js +taxonomy-with-ids.de-DE* + # Local development notes dev-notes.md dev-notes.local.md \ No newline at end of file diff --git a/prerender/seo/feeds.cjs b/prerender/seo/feeds.cjs index e193cc1..b24a56b 100644 --- a/prerender/seo/feeds.cjs +++ b/prerender/seo/feeds.cjs @@ -48,131 +48,131 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => { const getGoogleProductCategory = (categoryId) => { const categoryMappings = { // Seeds & Plants - 689: "Home & Garden > Plants > Seeds", - 706: "Home & Garden > Plants", // Stecklinge (cuttings) - 376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets + 689: "543561", // Seeds (Saatgut) + 706: "543561", // Stecklinge (cuttings) – ebenfalls Pflanzen/Saatgut + 376: "2802", // Grow-Sets – Pflanzen- & Kräuteranbausets // Headshop & Accessories - 709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop - 711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs - 714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör - 748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe - 749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen - 896: "Electronics > Electronics Accessories", // Vaporizer - 710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder + 709: "4082", // Headshop – Rauchzubehör + 711: "4082", // Headshop > Bongs – Rauchzubehör + 714: "4082", // Headshop > Bongs > Zubehör – Rauchzubehör + 748: "4082", // Headshop > Bongs > Köpfe – Rauchzubehör + 749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen – Rauchzubehör + 896: "3151", // Headshop > Vaporizer – Vaporizer + 710: "5109", // Headshop > Grinder – Gewürzmühlen (Küchenhelfer) // Measuring & Packaging - 186: "Business & Industrial > Science & Laboratory", // Wiegen & Verpacken - 187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen - 346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel - 355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost - 407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags - 449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen - 539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen + 186: "5631", // Headshop > Wiegen & Verpacken – Aufbewahrung/Zubehör + 187: "4767", // Headshop > Waagen – Personenwaagen (Medizinisch) + 346: "7118", // Headshop > Vakuumbeutel – Vakuumierer-Beutel + 355: "606", // Headshop > Boveda & Integra Boost – Luftentfeuchter (nächstmögliche) + 407: "3561", // Headshop > Grove Bags – Aufbewahrungsbehälter + 449: "1496", // Headshop > Cliptütchen – Lebensmittelverpackungsmaterial + 539: "3110", // Headshop > Gläser & Dosen – Lebensmittelbehälter // Lighting & Equipment - 694: "Home & Garden > Lighting", // Lampen - 261: "Home & Garden > Lighting", // Lampenzubehör + 694: "3006", // Lampen – Lampen (Beleuchtung) + 261: "3006", // Zubehör > Lampenzubehör – Lampen // Plants & Growing - 691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - 692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör - 693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte + 691: "500033", // Dünger – Dünger + 692: "5633", // Zubehör > Dünger-Zubehör – Zubehör für Gartenarbeit + 693: "5655", // Zelte – Zelte // Pots & Containers - 219: "Home & Garden > Decor > Planters & Pots", // Töpfe - 220: "Home & Garden > Decor > Planters & Pots", // Untersetzer - 301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe - 317: "Home & Garden > Decor > Planters & Pots", // Air-Pot - 364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe - 292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische + 219: "113", // Töpfe – Blumentöpfe & Pflanzgefäße + 220: "3173", // Töpfe > Untersetzer – Gartentopfuntersetzer und Trays + 301: "113", // Töpfe > Stofftöpfe – (Blumentöpfe/Pflanzgefäße) + 317: "113", // Töpfe > Air-Pot – (Blumentöpfe/Pflanzgefäße) + 364: "113", // Töpfe > Kunststofftöpfe – (Blumentöpfe/Pflanzgefäße) + 292: "3568", // Bewässerung > Trays & Fluttische – Bewässerungssysteme // Ventilation & Climate - 703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets - 247: "Home & Garden > Outdoor Power Tools", // Belüftung - 214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren - 308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft - 609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer - 248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter - 392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter - 658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter - 310: "Home & Garden > Climate Control > Heating", // Heizmatten - 379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation + 703: "2802", // Grow-Sets > Abluft-Sets – (verwendet Pflanzen-Kräuter-Anbausets) + 247: "1700", // Belüftung – Ventilatoren (Klimatisierung) + 214: "1700", // Belüftung > Umluft-Ventilatoren – Ventilatoren + 308: "1700", // Belüftung > Ab- und Zuluft – Ventilatoren + 609: "1700", // Belüftung > Ab- und Zuluft > Schalldämpfer – Ventilatoren + 248: "1700", // Belüftung > Aktivkohlefilter – Ventilatoren (nächstmögliche) + 392: "1700", // Belüftung > Ab- und Zuluft > Zuluftfilter – Ventilatoren + 658: "606", // Belüftung > Luftbe- und -entfeuchter – Luftentfeuchter + 310: "2802", // Anzucht > Heizmatten – Pflanzen- & Kräuteranbausets + 379: "5631", // Belüftung > Geruchsneutralisation – Haushaltsbedarf: Aufbewahrung // Irrigation & Watering - 221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung - 250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche - 297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen - 354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher - 372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot - 389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat - 405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche - 425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks - 480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer - 519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher + 221: "3568", // Bewässerung – Bewässerungssysteme (Gesamt) + 250: "6318", // Bewässerung > Schläuche – Gartenschläuche + 297: "500100", // Bewässerung > Pumpen – Bewässerung-/Sprinklerpumpen + 354: "3780", // Bewässerung > Sprüher – Sprinkler & Sprühköpfe + 372: "3568", // Bewässerung > AutoPot – Bewässerungssysteme + 389: "3568", // Bewässerung > Blumat – Bewässerungssysteme + 405: "6318", // Bewässerung > Schläuche – Gartenschläuche + 425: "3568", // Bewässerung > Wassertanks – Bewässerungssysteme + 480: "3568", // Bewässerung > Tropfer – Bewässerungssysteme + 519: "3568", // Bewässerung > Pumpsprüher – Bewässerungssysteme // Growing Media & Soils - 242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden - 243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde - 269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos - 580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton + 242: "543677", // Böden – Gartenerde + 243: "543677", // Böden > Erde – Gartenerde + 269: "543677", // Böden > Kokos – Gartenerde + 580: "543677", // Böden > Perlite & Blähton – Gartenerde // Propagation & Starting - 286: "Home & Garden > Plants", // Anzucht - 298: "Home & Garden > Plants", // Steinwolltrays - 421: "Home & Garden > Plants", // Vermehrungszubehör - 489: "Home & Garden > Plants", // EazyPlug & Jiffy - 359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser + 286: "2802", // Anzucht – Pflanzen- & Kräuteranbausets + 298: "2802", // Anzucht > Steinwolltrays – Pflanzen- & Kräuteranbausets + 421: "2802", // Anzucht > Vermehrungszubehör – Pflanzen- & Kräuteranbausets + 489: "2802", // Anzucht > EazyPlug & Jiffy – Pflanzen- & Kräuteranbausets + 359: "3103", // Anzucht > Gewächshäuser – Gewächshäuser // Tools & Equipment - 373: "Home & Garden > Tools > Hand Tools", // GrowTool - 403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr - 259: "Home & Garden > Tools > Hand Tools", // Pressen - 280: "Home & Garden > Tools > Hand Tools", // Erntescheeren - 258: "Home & Garden > Tools", // Ernte & Verarbeitung - 278: "Home & Garden > Tools", // Extraktion - 302: "Home & Garden > Tools", // Erntemaschinen + 373: "3568", // Bewässerung > GrowTool – Bewässerungssysteme + 403: "3999", // Bewässerung > Messbecher & mehr – Messbecher & Dosierlöffel + 259: "756", // Zubehör > Ernte & Verarbeitung > Pressen – Nudelmaschinen + 280: "2948", // Zubehör > Ernte & Verarbeitung > Erntescheeren – Küchenmesser + 258: "684", // Zubehör > Ernte & Verarbeitung – Abfallzerkleinerer + 278: "5057", // Zubehör > Ernte & Verarbeitung > Extraktion – Slush-Eis-Maschinen + 302: "7332", // Zubehör > Ernte & Verarbeitung > Erntemaschinen – Gartenmaschinen // Hardware & Plumbing - 222: "Hardware > Plumbing Fixtures", // PE-Teile - 374: "Hardware > Plumbing Fixtures", // Verbindungsteile + 222: "3568", // Bewässerung > PE-Teile – Bewässerungssysteme + 374: "1700", // Belüftung > Ab- und Zuluft > Verbindungsteile – Ventilatoren // Electronics & Control - 314: "Electronics > Electronics Accessories", // Steuergeräte - 408: "Electronics > Electronics Accessories", // GrowControl - 344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte - 555: "Business & Industrial > Science & Laboratory > Lab Equipment", // Mikroskope + 314: "1700", // Belüftung > Steuergeräte – Ventilatoren + 408: "1700", // Belüftung > Steuergeräte > GrowControl – Ventilatoren + 344: "1207", // Zubehör > Messgeräte – Messwerkzeuge & Messwertgeber + 555: "4555", // Zubehör > Anbauzubehör > Mikroskope – Mikroskope // Camping & Outdoor - 226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör + 226: "5655", // Zubehör > Zeltzubehör – Zelte // Plant Care & Protection - 239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz - 240: "Home & Garden > Plants", // Anbauzubehör + 239: "4085", // Zubehör > Anbauzubehör > Pflanzenschutz – Herbizide + 240: "5633", // Zubehör > Anbauzubehör – Zubehör für Gartenarbeit // Office & Media - 424: "Business & Industrial > Office Supplies", // Etiketten & Schilder - 387: "Media > Books", // Literatur + 424: "4377", // Zubehör > Anbauzubehör > Etiketten & Schilder – Etiketten & Anhängerschilder + 387: "543541", // Zubehör > Anbauzubehör > Literatur – Bücher // General categories - 705: "Home & Garden", // Set-Konfigurator - 686: "Home & Garden", // Zubehör - 741: "Home & Garden", // Zubehör - 294: "Home & Garden", // Zubehör - 695: "Home & Garden", // Zubehör - 293: "Home & Garden", // Trockennetze - 4: "Home & Garden", // Sonstiges - 450: "Home & Garden", // Restposten + 705: "2802", // Grow-Sets > Set-Konfigurator – (ebenfalls Pflanzen-Anbausets) + 686: "1700", // Belüftung > Aktivkohlefilter > Zubehör – Ventilatoren + 741: "1700", // Belüftung > Ab- und Zuluft > Zubehör – Ventilatoren + 294: "3568", // Bewässerung > Zubehör – Bewässerungssysteme + 695: "5631", // Zubehör – Haushaltsbedarf: Aufbewahrung + 293: "5631", // Zubehör > Ernte & Verarbeitung > Trockennetze – Haushaltsbedarf: Aufbewahrung + 4: "5631", // Zubehör > Anbauzubehör > Sonstiges – Haushaltsbedarf: Aufbewahrung + 450: "5631", // Zubehör > Anbauzubehör > Restposten – Haushaltsbedarf: Aufbewahrung }; - const category = categoryMappings[categoryId] || "Home & Garden > Plants"; + const categoryId_str = categoryMappings[categoryId] || "5631"; // Default to Haushaltsbedarf: Aufbewahrung - // Validate that the category is not empty or too generic - if (!category || category.trim() === "") { - return "Home & Garden > Plants"; + // Validate that the category ID is not empty + if (!categoryId_str || categoryId_str.trim() === "") { + return "5631"; // Haushaltsbedarf: Aufbewahrung } - return category; + return categoryId_str; }; let productsXml = ` diff --git a/src/PrerenderNotFound.js b/src/PrerenderNotFound.js new file mode 100644 index 0000000..835be4f --- /dev/null +++ b/src/PrerenderNotFound.js @@ -0,0 +1,92 @@ +const React = require('react'); +const { + Box, + AppBar, + Toolbar, + Container +} = require('@mui/material'); +const Footer = require('./components/Footer.js').default; +const { Logo } = require('./components/header/index.js'); +const NotFound404 = require('./pages/NotFound404.js').default; + +class PrerenderNotFound extends React.Component { + render() { + return React.createElement( + Box, + { + sx: { + display: 'flex', + flexDirection: 'column', + minHeight: '100vh', + mb: 0, + pb: 0, + bgcolor: 'background.default' + } + }, + React.createElement( + AppBar, + { position: 'sticky', color: 'primary', elevation: 0, sx: { zIndex: 1100 } }, + React.createElement( + Toolbar, + { sx: { minHeight: 64, py: { xs: 0.5, sm: 0 } } }, + React.createElement( + Container, + { + maxWidth: 'lg', + sx: { + display: 'flex', + alignItems: 'center', + px: { xs: 0, sm: 3 } + } + }, + React.createElement( + Box, + { sx: { + display: 'flex', + alignItems: 'center', + width: '100%', + flexDirection: { xs: 'column', sm: 'row' } + } + }, + React.createElement( + Box, + { sx: { + display: 'flex', + alignItems: 'center', + width: '100%', + justifyContent: { xs: 'space-between', sm: 'flex-start' }, + minHeight: { xs: 52, sm: 'auto' }, + px: { xs: 0, sm: 0 } + } + }, + React.createElement(Logo) + ), + // Reserve space for SearchBar on mobile (invisible placeholder) + React.createElement( + Box, + { sx: { + display: { xs: 'block', sm: 'none' }, + width: '100%', + mt: { xs: 1, sm: 0 }, + mb: { xs: 0.5, sm: 0 }, + px: { xs: 0, sm: 0 }, + height: 40, // Reserve space for SearchBar + opacity: 0 // Invisible placeholder + } + } + ) + ) + ) + ) + ), + React.createElement( + Box, + { sx: { flexGrow: 1 } }, + React.createElement(NotFound404) + ), + React.createElement(Footer) + ); + } +} + +module.exports = { default: PrerenderNotFound }; \ No newline at end of file diff --git a/src/pages/NotFound404.js b/src/pages/NotFound404.js index b25e9f6..c4dcda2 100644 --- a/src/pages/NotFound404.js +++ b/src/pages/NotFound404.js @@ -16,19 +16,20 @@ const NotFound404 = () => { src="/assets/images/404.png" alt="404 - Page Not Found" style={{ + width: '300px', + height: '300px', maxWidth: '100%', - height: 'auto', - maxHeight: '300px', + display: 'block', }} /> - This page is no longer available. + Diese Seite scheint es nicht mehr zu geben. ); - return ; + return ; }; export default NotFound404; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 453daec..831a1c0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -312,27 +312,210 @@ export default { // Add middleware to handle /404 route BEFORE webpack-dev-server processing middlewares.unshift({ name: 'handle-404-route', - middleware: (req, res, next) => { + middleware: async (req, res, next) => { if (req.url === '/404') { - // Mark this request as a 404 and intercept the response - res.locals.is404 = true; + // Set up prerender environment + const { createRequire } = await import('module'); + const require = createRequire(import.meta.url); - // Override writeHead to force 404 status - const originalWriteHead = res.writeHead; - res.writeHead = function(statusCode, statusMessage, headers) { - // Force 404 status and no-cache headers - const newHeaders = { - ...headers, - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0' + require('@babel/register')({ + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-react' + ], + extensions: ['.js', '.jsx'], + ignore: [/node_modules/] + }); + + // Import React first and make it globally available + const React = require('react'); + global.React = React; // Make React available globally for components that don't import it + + // Set up minimal globals for prerender + if (!global.window) { + global.window = {}; + } + if (!global.navigator) { + global.navigator = { userAgent: 'node.js' }; + } + if (!global.URL) { + global.URL = require('url').URL; + } + if (!global.Blob) { + global.Blob = class MockBlob { + constructor(data, options) { + this.data = data; + this.type = options?.type || ''; + } }; - return originalWriteHead.call(this, 404, statusMessage, newHeaders); + } + + // Mock browser storage APIs + const mockStorage = { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + clear: () => {}, + key: () => null, + length: 0 }; - // Rewrite the URL to / so historyApiFallback can handle it - req.url = '/'; - next(); + if (!global.localStorage) { + global.localStorage = mockStorage; + } + if (!global.sessionStorage) { + global.sessionStorage = mockStorage; + } + + // Also add to window object for components that access it via window + global.window.localStorage = mockStorage; + global.window.sessionStorage = mockStorage; + + // Import the dedicated prerender component + const PrerenderNotFound = require('./src/PrerenderNotFound.js').default; + + // Create the prerender component + const component = React.createElement(PrerenderNotFound); + + // Get only the essential bundles (not lazy-loaded chunks) + let jsBundles = []; + try { + const outputFileSystem = devServer.compiler.outputFileSystem; + const outputPath = devServer.compiler.outputPath; + const jsPath = path.join(outputPath, 'js'); + + if (outputFileSystem.existsSync && outputFileSystem.existsSync(jsPath)) { + const jsFiles = outputFileSystem.readdirSync(jsPath); + + // Only include essential bundles in correct dependency order + const essentialBundles = []; + + // 1. Runtime bundle (webpack runtime - must be first) + const runtimeFile = jsFiles.find(f => f.startsWith('runtime.') && f.endsWith('.bundle.js')); + if (runtimeFile) essentialBundles.push('/js/' + runtimeFile); + + // 2. Vendor bundles (libraries that main depends on) + const reactFile = jsFiles.find(f => f.startsWith('react.') && f.endsWith('.bundle.js')); + if (reactFile) essentialBundles.push('/js/' + reactFile); + + const emotionFile = jsFiles.find(f => f.startsWith('emotion.') && f.endsWith('.bundle.js')); + if (emotionFile) essentialBundles.push('/js/' + emotionFile); + + const muiIconsCommonFile = jsFiles.find(f => f.startsWith('mui-icons-common.') && f.endsWith('.bundle.js')); + if (muiIconsCommonFile) essentialBundles.push('/js/' + muiIconsCommonFile); + + const muiCoreFile = jsFiles.find(f => f.startsWith('mui-core.') && f.endsWith('.bundle.js')); + if (muiCoreFile) essentialBundles.push('/js/' + muiCoreFile); + + const vendorFile = jsFiles.find(f => f.startsWith('vendor.') && f.endsWith('.bundle.js')); + if (vendorFile) essentialBundles.push('/js/' + vendorFile); + + // 3. Common shared code + const commonFile = jsFiles.find(f => f.startsWith('common.') && f.endsWith('.chunk.js')); + if (commonFile) essentialBundles.push('/js/' + commonFile); + + // 4. Main bundle (your app code - must be last) + const mainFile = jsFiles.find(f => f.startsWith('main.') && f.endsWith('.bundle.js')); + if (mainFile) essentialBundles.push('/js/' + mainFile); + + jsBundles = essentialBundles; + } + } catch (error) { + console.warn('Could not read webpack output filesystem:', error.message); + } + + // Fallback if we can't read the filesystem + if (jsBundles.length === 0) { + jsBundles = ['/js/runtime.bundle.js', '/js/main.bundle.js']; + } + + // Render the page in memory only (no file writing in dev mode) + 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; + + // Create fresh Emotion cache for this page + const cache = createEmotionCache(); + const { extractCriticalToChunks } = createEmotionServer(cache); + + // Wrap with StaticRouter to provide React Router context for Logo's Link components + const routedComponent = React.createElement( + StaticRouter, + { location: '/404' }, + component + ); + + const pageElement = React.createElement( + CacheProvider, + { value: cache }, + React.createElement(ThemeProvider, { theme: theme }, routedComponent) + ); + + // Render to string + const renderedMarkup = ReactDOMServer.renderToString(pageElement); + const emotionChunks = extractCriticalToChunks(renderedMarkup); + + // Build the full HTML page + const templatePath = path.resolve(__dirname, 'public', 'index.html'); + let template = fs.readFileSync(templatePath, 'utf8'); + + // Add JavaScript bundles + let scriptTags = ''; + jsBundles.forEach(jsFile => { + scriptTags += ``; + }); + + // Add global CSS from src/index.css + let globalCss = ''; + try { + globalCss = fs.readFileSync(path.resolve(__dirname, 'src', 'index.css'), 'utf8'); + // Fix relative font paths for prerendered HTML (remove ../public to make them relative to public root) + globalCss = globalCss.replace(/url\('\.\.\/public/g, "url('"); + } catch (error) { + console.warn('Could not read src/index.css:', error.message); + } + + // Add inline CSS from emotion + let emotionCss = ''; + if (emotionChunks.styles.length > 0) { + emotionChunks.styles.forEach(style => { + if (style.css) { + emotionCss += style.css; + } + }); + } + + // Combine all CSS + const inlineCss = globalCss + emotionCss; + + // Use the rendered markup as-is (no regex replacements) + let processedMarkup = renderedMarkup; + + // Replace placeholders in template + const finalHtml = template + .replace('
', `
${processedMarkup}
`) + .replace('', ``) + .replace('', ` + + ${scriptTags} + `); + + // Serve the prerendered HTML with 404 status + res.status(404); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + return res.send(finalHtml); + + // If we get here, prerender failed - let the error bubble up + throw new Error('404 prerender failed completely'); } else { next(); }