feat: add Hersteller page with manufacturer data fetching and SEO support

This commit is contained in:
sebseb7
2026-04-21 16:04:11 +02:00
parent 2c0b7aa84d
commit 66a1efd87b
10 changed files with 690 additions and 35 deletions

View File

@@ -140,6 +140,38 @@ const fetchCategoryImage = (socket, categoryId) => {
});
};
const fetchManufacturers = (socket) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Timeout fetching manufacturers"));
}, 10000);
socket.emit("getHerstellerImages", {}, (response) => {
clearTimeout(timeout);
if (response?.success && Array.isArray(response.manufacturers)) {
// Filter and format manufacturers similar to HerstellerPage.js
const manufacturers = response.manufacturers
.filter(m => m.imageBuffer)
.map(m => ({
id: m.id,
name: m.name || '',
slug: m.slug || '',
imageBuffer: m.imageBuffer,
}))
.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
resolve(manufacturers);
} else {
reject(
new Error(
`Invalid manufacturers response: ${JSON.stringify(response)}`
)
);
}
});
});
};
const saveProductImages = async (socket, products, categoryName, outputDir) => {
if (!products || products.length === 0) return;
@@ -383,6 +415,7 @@ module.exports = {
fetchProductDetails,
fetchProductImage,
fetchCategoryImage,
fetchManufacturers,
saveProductImages,
saveCategoryImages,
};

View File

@@ -18,7 +18,8 @@ const renderPage = (
needsRouter = false,
config,
suppressLogs = false,
productData = null
productData = null,
manufacturerData = null
) => {
const {
isProduction,
@@ -171,22 +172,44 @@ const renderPage = (
</script>
`;
// @note Create script to populate window.productCache with ONLY the static category tree
// @note Create script to populate window.productCache with static category tree and herstellerImages
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 hasCategoryCache = typeof global !== "undefined" && global.window && global.window.categoryCache;
const hasManufacturerData = manufacturerData && manufacturerData.length > 0;
console.log(" 📦 [" + filename + "] manufacturerData =", manufacturerData ? (manufacturerData.length + " items") : "null");
if (hasCategoryCache || hasManufacturerData) {
const cacheData = {};
// Add static categoryTree_209
if (hasCategoryCache && global.window.categoryCache["209_de"]) {
cacheData["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>
`;
// Add herstellerImages
if (hasManufacturerData) {
cacheData.herstellerImages = manufacturerData;
}
const cacheDataJson = JSON.stringify(cacheData);
let extraScripts = '';
if (hasCategoryCache && cacheData["209_de"]) {
const categoryCacheJson = JSON.stringify({ "209_de": cacheData["209_de"] });
extraScripts += 'window.categoryCache = ' + categoryCacheJson + ';';
}
if (hasManufacturerData) {
const herstellerJson = JSON.stringify(manufacturerData);
extraScripts += 'window.herstellerImages = ' + herstellerJson + ';';
}
productCacheScript = '<script>' +
'if (!window.productCache) { window.productCache = {}; }' +
'Object.assign(window.productCache, ' + cacheDataJson + ');' +
extraScripts +
'</script>';
}
// Create script to populate window.productDetailCache for individual product pages

View File

@@ -0,0 +1,116 @@
/** Safe for double-quoted HTML attributes */
const escAttr = (str) =>
String(str ?? "")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;");
/**
* Head tags for prerendered Hersteller (Manufacturers) page
*/
const generateHerstellerMetaTags = (baseUrl, config, manufacturerCount = 0) => {
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
const herstellerUrl = root + "/Hersteller";
const site = config.siteName || config.brandName;
const desc = manufacturerCount + " Hersteller bei " + config.brandName + ": Top-Marken für Growshop-Produkte. Schnelle Lieferung, Laden Dresden.";
const descShort = desc.length > 160 ? desc.slice(0, 157) + "..." : desc;
const e = escAttr;
const logoUrl =
config.images && config.images.logo
? root + config.images.logo
: root + "/assets/images/nopicture.jpg";
return `
<meta name="description" content="${e(descShort)}">
<meta property="og:title" content="${e("Hersteller | " + site)}">
<meta property="og:description" content="${e(descShort)}">
<meta property="og:url" content="${herstellerUrl}">
<meta property="og:type" content="website">
<meta property="og:image" content="${e(logoUrl)}">
<meta property="og:site_name" content="${e(site)}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${e("Hersteller | " + site)}">
<meta name="twitter:description" content="${e(descShort)}">
<meta name="robots" content="index, follow">
<link rel="canonical" href="${herstellerUrl}">
`;
};
const generateHerstellerJsonLd = (baseUrl, config) => {
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
const herstellerUrl = root + "/Hersteller";
const id = {
business: root + "#business",
website: root + "#website",
breadcrumb: herstellerUrl + "#breadcrumb",
};
const logoUrl =
config.images && config.images.logo
? root + config.images.logo
: undefined;
const businessNode = {
"@id": id.business,
"@type": ["GardenStore", "LocalBusiness", "Organization"],
name: config.brandName,
url: root,
};
if (logoUrl) {
businessNode.logo = { "@type": "ImageObject", url: logoUrl };
businessNode.image = { "@type": "ImageObject", url: logoUrl };
}
const websiteNode = {
"@id": id.website,
"@type": "WebSite",
name: config.siteName || config.brandName,
url: root,
publisher: { "@id": id.business },
};
const breadcrumbNode = {
"@id": id.breadcrumb,
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: root,
},
{
"@type": "ListItem",
position: 2,
name: "Hersteller",
item: herstellerUrl,
},
],
};
const collectionPageNode = {
"@id": herstellerUrl,
"@type": "CollectionPage",
name: "Hersteller",
url: herstellerUrl,
description: "Alle Hersteller und Marken für Growshop-Produkte",
isPartOf: { "@id": id.website },
breadcrumb: { "@id": id.breadcrumb },
};
const graph = [businessNode, websiteNode, breadcrumbNode, collectionPageNode];
const herstellerGraph = {
"@context": "https://schema.org",
"@graph": graph,
};
return "<script type=\"application/ld+json\">" + JSON.stringify(herstellerGraph) + "</script>";
};
module.exports = {
generateHerstellerMetaTags,
generateHerstellerJsonLd,
};

View File

@@ -23,6 +23,11 @@ const {
generateKonfiguratorMetaTags,
} = require('./konfigurator.cjs');
const {
generateHerstellerMetaTags,
generateHerstellerJsonLd,
} = require('./hersteller.cjs');
const {
generateRobotsTxt,
generateProductsXml,
@@ -56,6 +61,10 @@ module.exports = {
// Konfigurator functions
generateKonfiguratorMetaTags,
// Hersteller functions
generateHerstellerMetaTags,
generateHerstellerJsonLd,
// Feed/Export functions
generateRobotsTxt,
generateProductsXml,