- Use absolute og:image URLs and add missing og:description on Impressum - Load Google Fonts via <link> instead of blocking @import - Prevent default on shop request button to avoid stray URL hash - Guard optimize-images.js against silent filename collisions - Add real Umsatzsteuer-Identifikationsnummer (UID) to Impressum - Correct README's shop description to match actual empty state - Stop tracking images/fotowebsite/ (raw, uncompressed source photos) since only images/optimized/ is served by the site
100 lines
3.1 KiB
JavaScript
100 lines
3.1 KiB
JavaScript
/**
|
||
* Image optimization script for Tischlerei Staffenberger.
|
||
* Generates WebP + JPEG variants at thumbnail (800px) and lightbox (1920px) sizes.
|
||
* Output: images/optimized/{category}/{name}-{size}w.{ext}
|
||
*
|
||
* Run: node scripts/optimize-images.js
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const sharp = require('sharp');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const PROJECT_ROOT = path.join(__dirname, '..');
|
||
const INPUT_ROOT = path.join(PROJECT_ROOT, 'images', 'fotowebsite');
|
||
const OUTPUT_ROOT = path.join(PROJECT_ROOT, 'images', 'optimized');
|
||
|
||
// Gallery categories: [input subdir, output subdir]
|
||
const GALLERY_DIRS = [
|
||
['kitchen', 'kitchen'],
|
||
['outdoor', 'outdoor'],
|
||
['custom', 'custom'],
|
||
];
|
||
|
||
const GALLERY_SIZES = [800, 1920];
|
||
const BANNER_SIZES = [800, 1920];
|
||
|
||
function safeName(filename) {
|
||
return path.basename(filename, path.extname(filename)).replace(/\s+/g, '-');
|
||
}
|
||
|
||
async function processImage(inputPath, outputDir, baseName, sizes) {
|
||
fs.mkdirSync(outputDir, { recursive: true });
|
||
|
||
for (const width of sizes) {
|
||
const jpegOut = path.join(outputDir, `${baseName}-${width}w.jpg`);
|
||
const webpOut = path.join(outputDir, `${baseName}-${width}w.webp`);
|
||
|
||
await sharp(inputPath)
|
||
.resize({ width, withoutEnlargement: true })
|
||
.jpeg({ quality: 85, mozjpeg: true })
|
||
.toFile(jpegOut);
|
||
|
||
await sharp(inputPath)
|
||
.resize({ width, withoutEnlargement: true })
|
||
.webp({ quality: 82 })
|
||
.toFile(webpOut);
|
||
|
||
const jpegSize = (fs.statSync(jpegOut).size / 1024).toFixed(0);
|
||
const webpSize = (fs.statSync(webpOut).size / 1024).toFixed(0);
|
||
console.log(` ${path.basename(jpegOut)}: JPEG ${jpegSize}KB WebP ${webpSize}KB`);
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
console.log('=== Tischlerei Staffenberger – Image Optimization ===\n');
|
||
|
||
// --- Gallery images ---
|
||
for (const [inputSubdir, outputSubdir] of GALLERY_DIRS) {
|
||
const inputDir = path.join(INPUT_ROOT, inputSubdir);
|
||
const outputDir = path.join(OUTPUT_ROOT, outputSubdir);
|
||
|
||
const files = fs.readdirSync(inputDir)
|
||
.filter(f => /\.(jpe?g)$/i.test(f))
|
||
.sort();
|
||
|
||
console.log(`[${inputSubdir}] – ${files.length} images`);
|
||
|
||
const usedNames = new Map(); // baseName -> source filename, to catch collisions
|
||
for (const file of files) {
|
||
const inputPath = path.join(inputDir, file);
|
||
const baseName = safeName(file);
|
||
|
||
const collidesWith = usedNames.get(baseName);
|
||
if (collidesWith) {
|
||
throw new Error(
|
||
`Filename collision in ${inputSubdir}: "${file}" and "${collidesWith}" both normalize to "${baseName}" ` +
|
||
`and would overwrite each other's optimized output. Rename one of the source files and re-run.`
|
||
);
|
||
}
|
||
usedNames.set(baseName, file);
|
||
|
||
await processImage(inputPath, outputDir, baseName, GALLERY_SIZES);
|
||
}
|
||
}
|
||
|
||
// --- Banner ---
|
||
const bannerInput = path.join(INPUT_ROOT, 'banner.jpg');
|
||
const bannerOutput = OUTPUT_ROOT;
|
||
console.log('\n[banner]');
|
||
await processImage(bannerInput, bannerOutput, 'banner', BANNER_SIZES);
|
||
|
||
console.log('\nDone.');
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
});
|