feat: Enhance image processing in data-fetching and update SEO meta tags for product images; add Telegram assistant link in ChatAssistant component with localization support
This commit is contained in:
@@ -186,80 +186,98 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
|
||||
.filter((id) => id);
|
||||
|
||||
if (imageIds.length > 0) {
|
||||
// Process first image for each product
|
||||
// Process first image for each product — store AVIF + JPEG (e.g. for Twitter / social)
|
||||
const bildId = parseInt(imageIds[0]);
|
||||
const estimatedFilename = `prod${bildId}.avif`; // We'll generate a filename based on the ID
|
||||
const avifFilename = `prod${bildId}.avif`;
|
||||
const jpegFilename = `prod${bildId}.jpg`;
|
||||
const avifPath = path.join(assetsPath, avifFilename);
|
||||
const jpegPath = path.join(assetsPath, jpegFilename);
|
||||
|
||||
const imagePath = path.join(assetsPath, estimatedFilename);
|
||||
|
||||
// Skip if image already exists
|
||||
if (fs.existsSync(imagePath)) {
|
||||
if (fs.existsSync(avifPath) && fs.existsSync(jpegPath)) {
|
||||
imagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const writeAvifAndJpegFromBuffer = async (buf) => {
|
||||
if (!fs.existsSync(avifPath)) {
|
||||
await sharp(buf).avif().toFile(avifPath);
|
||||
}
|
||||
if (!fs.existsSync(jpegPath)) {
|
||||
await sharp(buf)
|
||||
.jpeg({ quality: 85, mozjpeg: true })
|
||||
.toFile(jpegPath);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const imageBuffer = await fetchProductImage(socket, bildId);
|
||||
|
||||
// If overlay exists, apply it to the image
|
||||
if (false && fs.existsSync(overlayPath)) {
|
||||
try {
|
||||
// Get image dimensions to center the overlay
|
||||
const baseImage = sharp(Buffer.from(imageBuffer));
|
||||
const baseMetadata = await baseImage.metadata();
|
||||
|
||||
const overlaySize = Math.min(baseMetadata.width, baseMetadata.height) * 0.4;
|
||||
|
||||
// Resize overlay to 20% of base image size and get its buffer
|
||||
const resizedOverlayBuffer = await sharp(overlayPath)
|
||||
.resize({
|
||||
width: Math.round(overlaySize),
|
||||
height: Math.round(overlaySize),
|
||||
fit: 'contain', // Keep full overlay visible
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 } // Transparent background instead of black bars
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
// Calculate center position for the resized overlay
|
||||
const centerX = Math.floor((baseMetadata.width - overlaySize) / 2);
|
||||
const centerY = Math.floor((baseMetadata.height - overlaySize) / 2);
|
||||
|
||||
const processedImageBuffer = await baseImage
|
||||
.composite([
|
||||
{
|
||||
input: resizedOverlayBuffer,
|
||||
top: centerY,
|
||||
left: centerX,
|
||||
blend: "multiply", // Darkens the image, visible on all backgrounds
|
||||
opacity: 0.3,
|
||||
},
|
||||
])
|
||||
.avif() // Ensure output is AVIF
|
||||
.toBuffer();
|
||||
|
||||
fs.writeFileSync(imagePath, processedImageBuffer);
|
||||
console.log(
|
||||
` ✅ Applied centered inverted sh.avif overlay to ${estimatedFilename}`
|
||||
);
|
||||
} catch (overlayError) {
|
||||
console.log(
|
||||
` ⚠️ Failed to apply overlay to ${estimatedFilename}: ${overlayError.message}`
|
||||
);
|
||||
// Fallback: save without overlay
|
||||
fs.writeFileSync(imagePath, Buffer.from(imageBuffer));
|
||||
}
|
||||
if (fs.existsSync(avifPath) && !fs.existsSync(jpegPath)) {
|
||||
await sharp(avifPath)
|
||||
.jpeg({ quality: 85, mozjpeg: true })
|
||||
.toFile(jpegPath);
|
||||
} else if (!fs.existsSync(avifPath) && fs.existsSync(jpegPath)) {
|
||||
await sharp(jpegPath).avif().toFile(avifPath);
|
||||
} else {
|
||||
// Save without overlay if overlay file doesn't exist
|
||||
fs.writeFileSync(imagePath, Buffer.from(imageBuffer));
|
||||
const imageBuffer = await fetchProductImage(socket, bildId);
|
||||
const buf = Buffer.from(imageBuffer);
|
||||
|
||||
// If overlay exists, apply it to the image
|
||||
if (false && fs.existsSync(overlayPath)) {
|
||||
try {
|
||||
const baseImage = sharp(buf);
|
||||
const baseMetadata = await baseImage.metadata();
|
||||
|
||||
const overlaySize =
|
||||
Math.min(baseMetadata.width, baseMetadata.height) * 0.4;
|
||||
|
||||
const resizedOverlayBuffer = await sharp(overlayPath)
|
||||
.resize({
|
||||
width: Math.round(overlaySize),
|
||||
height: Math.round(overlaySize),
|
||||
fit: "contain",
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
const centerX = Math.floor(
|
||||
(baseMetadata.width - overlaySize) / 2
|
||||
);
|
||||
const centerY = Math.floor(
|
||||
(baseMetadata.height - overlaySize) / 2
|
||||
);
|
||||
|
||||
const processedImageBuffer = await baseImage
|
||||
.composite([
|
||||
{
|
||||
input: resizedOverlayBuffer,
|
||||
top: centerY,
|
||||
left: centerX,
|
||||
blend: "multiply",
|
||||
opacity: 0.3,
|
||||
},
|
||||
])
|
||||
.toBuffer();
|
||||
|
||||
await writeAvifAndJpegFromBuffer(processedImageBuffer);
|
||||
console.log(
|
||||
` ✅ Applied overlay → ${avifFilename} + ${jpegFilename}`
|
||||
);
|
||||
} catch (overlayError) {
|
||||
console.log(
|
||||
` ⚠️ Failed to apply overlay to prod${bildId}: ${overlayError.message}`
|
||||
);
|
||||
await writeAvifAndJpegFromBuffer(buf);
|
||||
}
|
||||
} else {
|
||||
await writeAvifAndJpegFromBuffer(buf);
|
||||
}
|
||||
}
|
||||
|
||||
imagesSaved++;
|
||||
|
||||
// Small delay to avoid overwhelming server
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
} catch (error) {
|
||||
console.log(
|
||||
` ⚠️ Failed to fetch image ${estimatedFilename} (ID: ${bildId}): ${error.message}`
|
||||
` ⚠️ Failed to fetch/save prod${bildId} (${avifFilename} / ${jpegFilename}): ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
const generateProductMetaTags = (product, baseUrl, config) => {
|
||||
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
|
||||
|
||||
const imageUrl =
|
||||
product.pictureList && product.pictureList.trim()
|
||||
? `${baseUrl}/assets/images/prod${product.pictureList
|
||||
.split(",")[0]
|
||||
.trim()}.avif`
|
||||
: `${baseUrl}/assets/images/nopicture.jpg`;
|
||||
|
||||
const pictureFirstId =
|
||||
product.pictureList && product.pictureList.trim()
|
||||
? product.pictureList.split(",")[0].trim()
|
||||
: null;
|
||||
|
||||
const imageUrl = pictureFirstId
|
||||
? `${baseUrl}/assets/images/prod${pictureFirstId}.avif`
|
||||
: `${baseUrl}/assets/images/nopicture.jpg`;
|
||||
|
||||
const twitterImageUrl = pictureFirstId
|
||||
? `${baseUrl}/assets/images/prod${pictureFirstId}.jpg`
|
||||
: `${baseUrl}/assets/images/nopicture.jpg`;
|
||||
|
||||
// Clean description for meta (remove HTML tags and limit length)
|
||||
const cleanDescription = product.kurzBeschreibung
|
||||
@@ -32,7 +37,7 @@ const generateProductMetaTags = (product, baseUrl, config) => {
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="${product.name}">
|
||||
<meta property="og:description" content="${cleanDescription}">
|
||||
<meta property="og:image" content="${imageUrl}">
|
||||
<meta property="og:image" content="${twitterImageUrl}">
|
||||
<meta property="og:url" content="${productUrl}">
|
||||
<meta property="og:type" content="product">
|
||||
<meta property="og:site_name" content="${config.siteName}">
|
||||
@@ -49,7 +54,7 @@ const generateProductMetaTags = (product, baseUrl, config) => {
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="${product.name}">
|
||||
<meta name="twitter:description" content="${cleanDescription}">
|
||||
<meta name="twitter:image" content="${imageUrl}">
|
||||
<meta name="twitter:image" content="${twitterImageUrl}">
|
||||
|
||||
<!-- Additional Meta Tags -->
|
||||
<meta name="robots" content="index, follow">
|
||||
@@ -64,12 +69,13 @@ const generateProductMetaTags = (product, baseUrl, config) => {
|
||||
|
||||
const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
|
||||
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
|
||||
const imageUrl =
|
||||
const pictureFirstId =
|
||||
product.pictureList && product.pictureList.trim()
|
||||
? `${baseUrl}/assets/images/prod${product.pictureList
|
||||
.split(",")[0]
|
||||
.trim()}.avif`
|
||||
: `${baseUrl}/assets/images/nopicture.jpg`;
|
||||
? product.pictureList.split(",")[0].trim()
|
||||
: null;
|
||||
const imageUrl = pictureFirstId
|
||||
? `${baseUrl}/assets/images/prod${pictureFirstId}.avif`
|
||||
: `${baseUrl}/assets/images/nopicture.jpg`;
|
||||
|
||||
// Clean description for JSON-LD (remove HTML tags)
|
||||
const cleanDescription = product.description
|
||||
|
||||
Reference in New Issue
Block a user