diff --git a/prerender/data-fetching.cjs b/prerender/data-fetching.cjs
index 0b1b6d0..09e2391 100644
--- a/prerender/data-fetching.cjs
+++ b/prerender/data-fetching.cjs
@@ -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}`
);
}
}
diff --git a/prerender/seo/product.cjs b/prerender/seo/product.cjs
index 01dad48..dc2e71e 100644
--- a/prerender/seo/product.cjs
+++ b/prerender/seo/product.cjs
@@ -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) => {
-
+
@@ -49,7 +54,7 @@ const generateProductMetaTags = (product, baseUrl, config) => {
-
+
@@ -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
diff --git a/src/components/ChatAssistant.js b/src/components/ChatAssistant.js
index 93b7c69..4a8b7c5 100644
--- a/src/components/ChatAssistant.js
+++ b/src/components/ChatAssistant.js
@@ -15,7 +15,13 @@ import StopIcon from '@mui/icons-material/Stop';
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
import parse, { domToReact } from 'html-react-parser';
import { Link } from 'react-router-dom';
+import MuiLink from '@mui/material/Link';
+import { alpha } from '@mui/material/styles';
+import TelegramIcon from '@mui/icons-material/Telegram';
import { isUserLoggedIn } from './LoginComponent.js';
+import { withTranslation } from '../i18n/withTranslation.js';
+
+const TELEGRAM_ASSISTANT_URL = 'https://t.me/Growheads_de_Bot';
// Initialize window object for storing messages
if (!window.chatMessages) {
window.chatMessages = [];
@@ -451,8 +457,9 @@ class ChatAssistant extends Component {
});
render() {
- const { open, onClose } = this.props;
+ const { open, onClose, t } = this.props;
const { messages, inputValue, isTyping, isRecording, recordingTime, isGuest, privacyConfirmed } = this.state;
+ const showTelegramHint = !messages.some((m) => m.sender === 'user');
if (!open) {
return null;
@@ -517,6 +524,58 @@ class ChatAssistant extends Component {
gap: 2,
}}
>
+ {showTelegramHint && (
+ alpha(theme.palette.primary.main, 0.14),
+ boxShadow: (theme) =>
+ `0 4px 14px ${alpha(theme.palette.primary.main, 0.35)}`,
+ }}
+ >
+
+
+ `drop-shadow(0 1px 2px ${alpha(theme.palette.primary.dark, 0.45)})`,
+ }}
+ />
+
+
+ {t('chat.telegramAssistantIntro')}
+
+
+ {t('chat.telegramAssistantLink')}
+
+
+
+
+ )}
{messages &&messages.map((message) => (