feat: add hero image processing and API for serving variants
- 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.
This commit is contained in:
157
scripts/prepare-hero-variants.mjs
Normal file
157
scripts/prepare-hero-variants.mjs
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user