Compare commits

...

2 Commits

Author SHA1 Message Date
sebseb7
ef91e50aa5 feat(Images): convert images to AVIF format for improved performance
- Updated image references in various components and configuration files to use AVIF format instead of PNG and JPG.
- Modified the build process to include a script for converting images to AVIF, enhancing loading times and reducing file sizes.
- Ensured consistency across the application by updating image paths in the footer, main layout, and content components.
2025-11-20 11:43:07 +01:00
sebseb7
061bf5ff17 feat(SEO): add price validity date to category JSON-LD
- Introduced a priceValidUntil field in the category JSON-LD to indicate the validity period of product prices, set to three months from the current date.
- This enhancement improves the structured data for SEO, providing clearer information about pricing timelines.
2025-11-20 11:13:48 +01:00
15 changed files with 109 additions and 11 deletions

View File

@@ -7,7 +7,7 @@
"start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open", "start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open", "start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool", "prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html", "build:client": "node scripts/convert-images-to-avif.cjs && cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
"build": "npm run build:client", "build": "npm run build:client",
"analyze": "cross-env ANALYZE=true NODE_ENV=production webpack --progress --mode production", "analyze": "cross-env ANALYZE=true NODE_ENV=production webpack --progress --mode production",
"lint": "eslint src/**/*.{js,jsx}", "lint": "eslint src/**/*.{js,jsx}",

View File

@@ -152,7 +152,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
"public", "public",
"assets", "assets",
"images", "images",
"sh.png" "sh.avif"
); );
// Ensure assets/images directory exists // Ensure assets/images directory exists
@@ -236,7 +236,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
fs.writeFileSync(imagePath, processedImageBuffer); fs.writeFileSync(imagePath, processedImageBuffer);
console.log( console.log(
` ✅ Applied centered inverted sh.png overlay to ${estimatedFilename}` ` ✅ Applied centered inverted sh.avif overlay to ${estimatedFilename}`
); );
} catch (overlayError) { } catch (overlayError) {
console.log( console.log(

View File

@@ -1,6 +1,19 @@
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
// Category IDs to skip (seeds, plants, headshop items)
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710, 924, 923, 922, 921, 916, 278, 259, 258];
// Check if category ID is in skip list
if (category.id && skipCategoryIds.includes(parseInt(category.id))) {
return '';
}
const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`; const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
// Calculate price valid date (current date + 3 months)
const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const priceValidUntil = priceValidDate.toISOString().split("T")[0];
const jsonLd = { const jsonLd = {
"@context": "https://schema.org/", "@context": "https://schema.org/",
"@type": "CollectionPage", "@type": "CollectionPage",
@@ -57,6 +70,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
url: `${baseUrl}/Artikel/${product.seoName}`, url: `${baseUrl}/Artikel/${product.seoName}`,
price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00", price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
priceCurrency: config.currency, priceCurrency: config.currency,
priceValidUntil: priceValidUntil,
availability: product.available availability: product.available
? "https://schema.org/InStock" ? "https://schema.org/InStock"
: "https://schema.org/OutOfStock", : "https://schema.org/OutOfStock",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,58 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const imagesToConvert = [
{ src: 'sh.png', dest: 'sh.avif' },
{ src: 'seeds.jpg', dest: 'seeds.avif' },
{ src: 'cutlings.jpg', dest: 'cutlings.avif' },
{ src: 'gg.png', dest: 'gg.avif' },
{ src: 'maps.png', dest: 'maps.avif' }
];
const run = async () => {
const imagesDir = path.join(__dirname, '../public/assets/images');
let hasError = false;
for (const image of imagesToConvert) {
const inputPath = path.join(imagesDir, image.src);
const outputPath = path.join(imagesDir, image.dest);
if (!fs.existsSync(inputPath)) {
console.warn(`⚠️ Input file not found: ${inputPath}`);
continue;
}
// Check if output file exists and compare modification times
// Only convert if source is newer or destination doesn't exist
let shouldConvert = true;
if (fs.existsSync(outputPath)) {
const inputStat = fs.statSync(inputPath);
const outputStat = fs.statSync(outputPath);
if (inputStat.mtime <= outputStat.mtime) {
shouldConvert = false;
}
}
if (shouldConvert) {
try {
await sharp(inputPath)
.toFormat('avif')
.toFile(outputPath);
console.log(`✅ Converted ${image.src} to ${image.dest}`);
} catch (error) {
console.error(`❌ Error converting ${image.src}:`, error.message);
hasError = true;
}
} else {
// Silent skip if already up to date to keep logs clean, or use verbose flag
// console.log(`Skipping ${image.src} (already up to date)`);
}
}
if (hasError) {
process.exit(1);
}
};
run();

View File

@@ -0,0 +1,26 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const run = async () => {
const inputPath = path.join(__dirname, '../public/assets/images/sh.png');
const outputPath = path.join(__dirname, '../public/assets/images/sh.avif');
if (!fs.existsSync(inputPath)) {
console.error('Input file not found:', inputPath);
process.exit(1);
}
try {
await sharp(inputPath)
.toFormat('avif')
.toFile(outputPath);
console.log(`Successfully converted ${inputPath} to ${outputPath}`);
} catch (error) {
console.error('Error converting image:', error);
process.exit(1);
}
};
run();

View File

@@ -722,7 +722,7 @@ class Content extends Component {
justifyContent: 'center' justifyContent: 'center'
}}> }}>
<img <img
src="/assets/images/seeds.jpg" src="/assets/images/seeds.avif"
alt="Seeds" alt="Seeds"
fetchPriority="high" fetchPriority="high"
loading="eager" loading="eager"
@@ -783,7 +783,7 @@ class Content extends Component {
justifyContent: 'center' justifyContent: 'center'
}}> }}>
<img <img
src="/assets/images/cutlings.jpg" src="/assets/images/cutlings.avif"
alt="Stecklinge" alt="Stecklinge"
fetchPriority="high" fetchPriority="high"
loading="eager" loading="eager"

View File

@@ -296,7 +296,7 @@ class Footer extends Component {
> >
<Box <Box
component="img" component="img"
src="/assets/images/gg.png" src="/assets/images/gg.avif"
alt="Google Reviews" alt="Google Reviews"
sx={{ sx={{
height: { xs: 50, md: 60 }, height: { xs: 50, md: 60 },
@@ -326,7 +326,7 @@ class Footer extends Component {
> >
<Box <Box
component="img" component="img"
src="/assets/images/maps.png" src="/assets/images/maps.avif"
alt="Google Maps" alt="Google Maps"
sx={{ sx={{
height: { xs: 40, md: 50 }, height: { xs: 40, md: 50 },

View File

@@ -163,8 +163,8 @@ const MainPageLayout = () => {
const allContentBoxes = { const allContentBoxes = {
home: [ home: [
{ title: t('sections.seeds'), image: "/assets/images/seeds.jpg", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" }, { title: t('sections.seeds'), image: "/assets/images/seeds.avif", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
{ title: t('sections.stecklinge'), image: "/assets/images/cutlings.jpg", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" } { title: t('sections.stecklinge'), image: "/assets/images/cutlings.avif", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" }
], ],
aktionen: [ aktionen: [
{ title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" }, { title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" },

View File

@@ -16,7 +16,7 @@ const Logo = () => {
}} }}
> >
<img <img
src="/assets/images/sh.png" src="/assets/images/sh.avif"
alt="SH Logo" alt="SH Logo"
width="108px" width="108px"
height="45px" height="45px"

View File

@@ -206,7 +206,7 @@ const config = {
// Images // Images
images: { images: {
logo: "/assets/images/sh.png", logo: "/assets/images/sh.avif",
placeholder: "/assets/images/nopicture.jpg" placeholder: "/assets/images/nopicture.jpg"
}, },