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.

This commit is contained in:
seb
2025-07-07 02:12:19 +02:00
parent 8698816875
commit 9e14827c91
5 changed files with 386 additions and 108 deletions

2
.gitignore vendored
View File

@@ -56,6 +56,8 @@ yarn-error.log*
# Local configuration # Local configuration
src/config.local.js src/config.local.js
taxonomy-with-ids.de-DE*
# Local development notes # Local development notes
dev-notes.md dev-notes.md
dev-notes.local.md dev-notes.local.md

View File

@@ -48,131 +48,131 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const getGoogleProductCategory = (categoryId) => { const getGoogleProductCategory = (categoryId) => {
const categoryMappings = { const categoryMappings = {
// Seeds & Plants // Seeds & Plants
689: "Home & Garden > Plants > Seeds", 689: "543561", // Seeds (Saatgut)
706: "Home & Garden > Plants", // Stecklinge (cuttings) 706: "543561", // Stecklinge (cuttings) ebenfalls Pflanzen/Saatgut
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets 376: "2802", // Grow-Sets Pflanzen- & Kräuteranbausets
// Headshop & Accessories // Headshop & Accessories
709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop 709: "4082", // Headshop Rauchzubehör
711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs 711: "4082", // Headshop > Bongs Rauchzubehör
714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör 714: "4082", // Headshop > Bongs > Zubehör Rauchzubehör
748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe 748: "4082", // Headshop > Bongs > Köpfe Rauchzubehör
749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen 749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen Rauchzubehör
896: "Electronics > Electronics Accessories", // Vaporizer 896: "3151", // Headshop > Vaporizer Vaporizer
710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder 710: "5109", // Headshop > Grinder Gewürzmühlen (Küchenhelfer)
// Measuring & Packaging // Measuring & Packaging
186: "Business & Industrial > Science & Laboratory", // Wiegen & Verpacken 186: "5631", // Headshop > Wiegen & Verpacken Aufbewahrung/Zubehör
187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen 187: "4767", // Headshop > Waagen Personenwaagen (Medizinisch)
346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel 346: "7118", // Headshop > Vakuumbeutel Vakuumierer-Beutel
355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost 355: "606", // Headshop > Boveda & Integra Boost Luftentfeuchter (nächstmögliche)
407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags 407: "3561", // Headshop > Grove Bags Aufbewahrungsbehälter
449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen 449: "1496", // Headshop > Cliptütchen Lebensmittelverpackungsmaterial
539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen 539: "3110", // Headshop > Gläser & Dosen Lebensmittelbehälter
// Lighting & Equipment // Lighting & Equipment
694: "Home & Garden > Lighting", // Lampen 694: "3006", // Lampen Lampen (Beleuchtung)
261: "Home & Garden > Lighting", // Lampenzubehör 261: "3006", // Zubehör > Lampenzubehör Lampen
// Plants & Growing // Plants & Growing
691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger 691: "500033", // Dünger Dünger
692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör 692: "5633", // Zubehör > Dünger-Zubehör Zubehör für Gartenarbeit
693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte 693: "5655", // Zelte Zelte
// Pots & Containers // Pots & Containers
219: "Home & Garden > Decor > Planters & Pots", // Töpfe 219: "113", // Töpfe Blumentöpfe & Pflanzgefäße
220: "Home & Garden > Decor > Planters & Pots", // Untersetzer 220: "3173", // Töpfe > Untersetzer Gartentopfuntersetzer und Trays
301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe 301: "113", // Töpfe > Stofftöpfe (Blumentöpfe/Pflanzgefäße)
317: "Home & Garden > Decor > Planters & Pots", // Air-Pot 317: "113", // Töpfe > Air-Pot (Blumentöpfe/Pflanzgefäße)
364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe 364: "113", // Töpfe > Kunststofftöpfe (Blumentöpfe/Pflanzgefäße)
292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische 292: "3568", // Bewässerung > Trays & Fluttische Bewässerungssysteme
// Ventilation & Climate // Ventilation & Climate
703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets 703: "2802", // Grow-Sets > Abluft-Sets (verwendet Pflanzen-Kräuter-Anbausets)
247: "Home & Garden > Outdoor Power Tools", // Belüftung 247: "1700", // Belüftung Ventilatoren (Klimatisierung)
214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren 214: "1700", // Belüftung > Umluft-Ventilatoren Ventilatoren
308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft 308: "1700", // Belüftung > Ab- und Zuluft Ventilatoren
609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer 609: "1700", // Belüftung > Ab- und Zuluft > Schalldämpfer Ventilatoren
248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter 248: "1700", // Belüftung > Aktivkohlefilter Ventilatoren (nächstmögliche)
392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter 392: "1700", // Belüftung > Ab- und Zuluft > Zuluftfilter Ventilatoren
658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter 658: "606", // Belüftung > Luftbe- und -entfeuchter Luftentfeuchter
310: "Home & Garden > Climate Control > Heating", // Heizmatten 310: "2802", // Anzucht > Heizmatten Pflanzen- & Kräuteranbausets
379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation 379: "5631", // Belüftung > Geruchsneutralisation Haushaltsbedarf: Aufbewahrung
// Irrigation & Watering // Irrigation & Watering
221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung 221: "3568", // Bewässerung Bewässerungssysteme (Gesamt)
250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche 250: "6318", // Bewässerung > Schläuche Gartenschläuche
297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen 297: "500100", // Bewässerung > Pumpen Bewässerung-/Sprinklerpumpen
354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher 354: "3780", // Bewässerung > Sprüher Sprinkler & Sprühköpfe
372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot 372: "3568", // Bewässerung > AutoPot Bewässerungssysteme
389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat 389: "3568", // Bewässerung > Blumat Bewässerungssysteme
405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche 405: "6318", // Bewässerung > Schläuche Gartenschläuche
425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks 425: "3568", // Bewässerung > Wassertanks Bewässerungssysteme
480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer 480: "3568", // Bewässerung > Tropfer Bewässerungssysteme
519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher 519: "3568", // Bewässerung > Pumpsprüher Bewässerungssysteme
// Growing Media & Soils // Growing Media & Soils
242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden 242: "543677", // Böden Gartenerde
243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde 243: "543677", // Böden > Erde Gartenerde
269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos 269: "543677", // Böden > Kokos Gartenerde
580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton 580: "543677", // Böden > Perlite & Blähton Gartenerde
// Propagation & Starting // Propagation & Starting
286: "Home & Garden > Plants", // Anzucht 286: "2802", // Anzucht Pflanzen- & Kräuteranbausets
298: "Home & Garden > Plants", // Steinwolltrays 298: "2802", // Anzucht > Steinwolltrays Pflanzen- & Kräuteranbausets
421: "Home & Garden > Plants", // Vermehrungszubehör 421: "2802", // Anzucht > Vermehrungszubehör Pflanzen- & Kräuteranbausets
489: "Home & Garden > Plants", // EazyPlug & Jiffy 489: "2802", // Anzucht > EazyPlug & Jiffy Pflanzen- & Kräuteranbausets
359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser 359: "3103", // Anzucht > Gewächshäuser Gewächshäuser
// Tools & Equipment // Tools & Equipment
373: "Home & Garden > Tools > Hand Tools", // GrowTool 373: "3568", // Bewässerung > GrowTool Bewässerungssysteme
403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr 403: "3999", // Bewässerung > Messbecher & mehr Messbecher & Dosierlöffel
259: "Home & Garden > Tools > Hand Tools", // Pressen 259: "756", // Zubehör > Ernte & Verarbeitung > Pressen Nudelmaschinen
280: "Home & Garden > Tools > Hand Tools", // Erntescheeren 280: "2948", // Zubehör > Ernte & Verarbeitung > Erntescheeren Küchenmesser
258: "Home & Garden > Tools", // Ernte & Verarbeitung 258: "684", // Zubehör > Ernte & Verarbeitung Abfallzerkleinerer
278: "Home & Garden > Tools", // Extraktion 278: "5057", // Zubehör > Ernte & Verarbeitung > Extraktion Slush-Eis-Maschinen
302: "Home & Garden > Tools", // Erntemaschinen 302: "7332", // Zubehör > Ernte & Verarbeitung > Erntemaschinen Gartenmaschinen
// Hardware & Plumbing // Hardware & Plumbing
222: "Hardware > Plumbing Fixtures", // PE-Teile 222: "3568", // Bewässerung > PE-Teile Bewässerungssysteme
374: "Hardware > Plumbing Fixtures", // Verbindungsteile 374: "1700", // Belüftung > Ab- und Zuluft > Verbindungsteile Ventilatoren
// Electronics & Control // Electronics & Control
314: "Electronics > Electronics Accessories", // Steuergeräte 314: "1700", // Belüftung > Steuergeräte Ventilatoren
408: "Electronics > Electronics Accessories", // GrowControl 408: "1700", // Belüftung > Steuergeräte > GrowControl Ventilatoren
344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte 344: "1207", // Zubehör > Messgeräte Messwerkzeuge & Messwertgeber
555: "Business & Industrial > Science & Laboratory > Lab Equipment", // Mikroskope 555: "4555", // Zubehör > Anbauzubehör > Mikroskope Mikroskope
// Camping & Outdoor // Camping & Outdoor
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör 226: "5655", // Zubehör > Zeltzubehör Zelte
// Plant Care & Protection // Plant Care & Protection
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz 239: "4085", // Zubehör > Anbauzubehör > Pflanzenschutz Herbizide
240: "Home & Garden > Plants", // Anbauzubehör 240: "5633", // Zubehör > Anbauzubehör Zubehör für Gartenarbeit
// Office & Media // Office & Media
424: "Business & Industrial > Office Supplies", // Etiketten & Schilder 424: "4377", // Zubehör > Anbauzubehör > Etiketten & Schilder Etiketten & Anhängerschilder
387: "Media > Books", // Literatur 387: "543541", // Zubehör > Anbauzubehör > Literatur Bücher
// General categories // General categories
705: "Home & Garden", // Set-Konfigurator 705: "2802", // Grow-Sets > Set-Konfigurator (ebenfalls Pflanzen-Anbausets)
686: "Home & Garden", // Zubehör 686: "1700", // Belüftung > Aktivkohlefilter > Zubehör Ventilatoren
741: "Home & Garden", // Zubehör 741: "1700", // Belüftung > Ab- und Zuluft > Zubehör Ventilatoren
294: "Home & Garden", // Zubehör 294: "3568", // Bewässerung > Zubehör Bewässerungssysteme
695: "Home & Garden", // Zubehör 695: "5631", // Zubehör Haushaltsbedarf: Aufbewahrung
293: "Home & Garden", // Trockennetze 293: "5631", // Zubehör > Ernte & Verarbeitung > Trockennetze Haushaltsbedarf: Aufbewahrung
4: "Home & Garden", // Sonstiges 4: "5631", // Zubehör > Anbauzubehör > Sonstiges Haushaltsbedarf: Aufbewahrung
450: "Home & Garden", // Restposten 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 // Validate that the category ID is not empty
if (!category || category.trim() === "") { if (!categoryId_str || categoryId_str.trim() === "") {
return "Home & Garden > Plants"; return "5631"; // Haushaltsbedarf: Aufbewahrung
} }
return category; return categoryId_str;
}; };
let productsXml = `<?xml version="1.0" encoding="UTF-8"?> let productsXml = `<?xml version="1.0" encoding="UTF-8"?>

92
src/PrerenderNotFound.js Normal file
View File

@@ -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 };

View File

@@ -16,19 +16,20 @@ const NotFound404 = () => {
src="/assets/images/404.png" src="/assets/images/404.png"
alt="404 - Page Not Found" alt="404 - Page Not Found"
style={{ style={{
width: '300px',
height: '300px',
maxWidth: '100%', maxWidth: '100%',
height: 'auto', display: 'block',
maxHeight: '300px',
}} }}
/> />
</Box> </Box>
<Typography variant="body1" paragraph align="center"> <Typography variant="body1" paragraph align="center">
This page is no longer available. Diese Seite scheint es nicht mehr zu geben.
</Typography> </Typography>
</> </>
); );
return <LegalPage title="Page Not Found" content={content} />; return <LegalPage content={content} />;
}; };
export default NotFound404; export default NotFound404;

View File

@@ -312,27 +312,210 @@ export default {
// Add middleware to handle /404 route BEFORE webpack-dev-server processing // Add middleware to handle /404 route BEFORE webpack-dev-server processing
middlewares.unshift({ middlewares.unshift({
name: 'handle-404-route', name: 'handle-404-route',
middleware: (req, res, next) => { middleware: async (req, res, next) => {
if (req.url === '/404') { if (req.url === '/404') {
// Mark this request as a 404 and intercept the response // Set up prerender environment
res.locals.is404 = true; const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
// Override writeHead to force 404 status require('@babel/register')({
const originalWriteHead = res.writeHead; presets: [
res.writeHead = function(statusCode, statusMessage, headers) { ['@babel/preset-env', { targets: { node: 'current' } }],
// Force 404 status and no-cache headers '@babel/preset-react'
const newHeaders = { ],
...headers, extensions: ['.js', '.jsx'],
'Cache-Control': 'no-cache, no-store, must-revalidate', ignore: [/node_modules/]
'Pragma': 'no-cache', });
'Expires': '0'
// 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 if (!global.localStorage) {
req.url = '/'; global.localStorage = mockStorage;
next(); }
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 += `<script src="${jsFile}"></script>`;
});
// 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('<div id="root"></div>', `<div id="root">${processedMarkup}</div>`)
.replace('</head>', `<style>${inlineCss}</style></head>`)
.replace('</body>', `
<script>
window.__PRERENDER_FALLBACK__ = {path: '/404', content: ${JSON.stringify(processedMarkup)}, timestamp: ${Date.now()}};
</script>
${scriptTags}
</body>`);
// 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 { } else {
next(); next();
} }