feat: add Hersteller page with manufacturer data fetching and SEO support
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
116
prerender/seo/hersteller.cjs
Normal file
116
prerender/seo/hersteller.cjs
Normal file
@@ -0,0 +1,116 @@
|
||||
/** Safe for double-quoted HTML attributes */
|
||||
const escAttr = (str) =>
|
||||
String(str ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<");
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user