- Introduced a new script `prepare-hero-variants.mjs` to generate responsive hero image variants in WebP format. - Added a fallback image `hero_fallback.png` for each variant. - Created an API endpoint `hero-images.get.js` to retrieve available hero image variants and their fallback images. - Implemented directory and file checks to ensure the existence of required images before serving.
158 lines
4.4 KiB
JavaScript
158 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
import { promises as fs } from 'fs'
|
|
import path from 'path'
|
|
import sharp from 'sharp'
|
|
|
|
const cwd = process.cwd()
|
|
const outputRoot = path.join(cwd, 'public', 'images', 'hero')
|
|
const inputRoots = [
|
|
path.join(cwd, 'assets', 'images', 'hero-originals'),
|
|
path.join(cwd, 'public', 'images', 'hero-originals')
|
|
]
|
|
|
|
const OUTPUT_VARIANTS = [
|
|
{ width: 960, height: 540, quality: 66, fileName: 'hero_960.webp' },
|
|
{ width: 1600, height: 900, quality: 72, fileName: 'hero_1600.webp' }
|
|
]
|
|
|
|
async function ensureDir(dirPath) {
|
|
await fs.mkdir(dirPath, { recursive: true })
|
|
}
|
|
|
|
async function listSubdirs(rootPath) {
|
|
const entries = await fs.readdir(rootPath, { withFileTypes: true })
|
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
|
}
|
|
|
|
async function listFiles(rootPath) {
|
|
const entries = await fs.readdir(rootPath, { withFileTypes: true })
|
|
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name)
|
|
}
|
|
|
|
function normalizeVariantName(name) {
|
|
return String(name || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_-]+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '') || 'hero'
|
|
}
|
|
|
|
function pickSourcePng(files) {
|
|
const pngFiles = files.filter((file) => /\.png$/i.test(file))
|
|
if (!pngFiles.length) return null
|
|
|
|
const prioritized = ['hero.png', 'hero-original.png', 'original.png']
|
|
for (const name of prioritized) {
|
|
const found = pngFiles.find((file) => file.toLowerCase() === name)
|
|
if (found) return found
|
|
}
|
|
|
|
return pngFiles.sort((a, b) => a.localeCompare(b, 'de'))[0]
|
|
}
|
|
|
|
async function convertVariant(sourcePath, targetDir, variant) {
|
|
const targetPath = path.join(targetDir, variant.fileName)
|
|
await sharp(sourcePath)
|
|
.resize(variant.width, variant.height, { fit: 'cover', position: 'centre' })
|
|
.webp({ quality: variant.quality })
|
|
.toFile(targetPath)
|
|
}
|
|
|
|
async function collectVariantSources(rootPath) {
|
|
const sources = []
|
|
|
|
const subdirs = await listSubdirs(rootPath)
|
|
for (const dirName of subdirs.sort((a, b) => a.localeCompare(b, 'de'))) {
|
|
const dirPath = path.join(rootPath, dirName)
|
|
const files = await fs.readdir(dirPath)
|
|
const sourceFile = pickSourcePng(files)
|
|
if (!sourceFile) continue
|
|
|
|
sources.push({
|
|
variant: normalizeVariantName(dirName),
|
|
sourcePath: path.join(dirPath, sourceFile)
|
|
})
|
|
}
|
|
|
|
if (sources.length) return sources
|
|
|
|
const rootFiles = await listFiles(rootPath)
|
|
const pngFiles = rootFiles.filter((file) => /\.png$/i.test(file)).sort((a, b) => a.localeCompare(b, 'de'))
|
|
for (const fileName of pngFiles) {
|
|
sources.push({
|
|
variant: normalizeVariantName(path.parse(fileName).name),
|
|
sourcePath: path.join(rootPath, fileName)
|
|
})
|
|
}
|
|
|
|
return sources
|
|
}
|
|
|
|
async function findInputRootWithSources() {
|
|
for (const rootPath of inputRoots) {
|
|
try {
|
|
const stats = await fs.stat(rootPath)
|
|
if (!stats.isDirectory()) continue
|
|
const sources = await collectVariantSources(rootPath)
|
|
if (sources.length) {
|
|
return { rootPath, sources }
|
|
}
|
|
} catch {
|
|
// ignore missing roots
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
async function processVariantSource(source) {
|
|
const targetDir = path.join(outputRoot, source.variant)
|
|
|
|
await ensureDir(targetDir)
|
|
|
|
for (const variant of OUTPUT_VARIANTS) {
|
|
await convertVariant(source.sourcePath, targetDir, variant)
|
|
}
|
|
|
|
await fs.copyFile(source.sourcePath, path.join(targetDir, 'hero_fallback.png'))
|
|
|
|
return {
|
|
variant: source.variant,
|
|
source: path.relative(cwd, source.sourcePath),
|
|
target: path.relative(cwd, targetDir)
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
await ensureDir(outputRoot)
|
|
|
|
const input = await findInputRootWithSources()
|
|
if (!input) {
|
|
console.error('Keine Hero-Quellen gefunden.')
|
|
console.error('Unterstuetzte Pfade:')
|
|
console.error(`- ${inputRoots[0]}/<variante>/*.png`)
|
|
console.error(`- ${inputRoots[1]}/*.png`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const results = []
|
|
for (const source of input.sources) {
|
|
const result = await processVariantSource(source)
|
|
results.push(result)
|
|
}
|
|
|
|
console.log(`Input-Quelle: ${path.relative(cwd, input.rootPath)}`)
|
|
console.log('Hero-Varianten erfolgreich erstellt:')
|
|
for (const result of results) {
|
|
console.log(`- ${result.variant}: ${result.source} -> ${result.target}`)
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler bei der Hero-Bildaufbereitung:', error)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
await main()
|