cms: Upload-Bilder zu WebP konvertieren (sharp)

Raster-Uploads (jpg/png/…) werden zu WebP (q82), auf max. 2000px begrenzt,
EXIF-Rotation korrigiert — ~85% kleiner. SVG/GIF bleiben unangetastet.
Eindeutige Dateinamen verhindern Kollisionen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:47:16 +02:00
parent f42a69c7ed
commit 5704c0f94c
3 changed files with 559 additions and 14 deletions
+32 -12
View File
@@ -1,11 +1,14 @@
import { Hono } from 'hono';
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import sharp from 'sharp';
// Bild-Upload → static/images/. Raster-Bilder werden zu WebP konvertiert
// (kleiner, web-optimiert), auf max. 2000px begrenzt, EXIF-Rotation korrigiert.
// SVG/GIF bleiben unangetastet (Vektor/Animation erhalten).
const SITE_DIR = process.env.SITE_DIR || '/site';
const PASSTHROUGH = new Set(['.svg', '.gif']);
// Bild-Upload → static/images/<name>. Hugo kopiert das beim Build nach
// public/images/, cover_image referenziert es als /images/<name>.
const upload = new Hono();
upload.post('/', async (c) => {
@@ -13,22 +16,39 @@ upload.post('/', async (c) => {
const file = body['file'];
if (!file || typeof file === 'string') return c.json({ error: 'Keine Datei' }, 400);
const name = safeName(file.name);
const dir = path.join(SITE_DIR, 'static', 'images');
await mkdir(dir, { recursive: true });
await writeFile(path.join(dir, name), Buffer.from(await file.arrayBuffer()));
return c.json({ url: `/images/${name}` });
const buffer = Buffer.from(await file.arrayBuffer());
const ext = path.extname(file.name || '').toLowerCase();
const base = `${safeBase(file.name)}-${uid()}`;
let outName, outBuf;
if (PASSTHROUGH.has(ext)) {
outName = `${base}${ext}`;
outBuf = buffer;
} else {
outName = `${base}.webp`;
outBuf = await sharp(buffer)
.rotate()
.resize({ width: 2000, withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer();
}
await writeFile(path.join(dir, outName), outBuf);
return c.json({ url: `/images/${outName}` });
});
// Sicherer Dateiname: nur basename, kleingeschrieben, ohne Pfad/Sonderzeichen.
function safeName(raw) {
const base = path.basename(String(raw || 'bild'));
const cleaned = base
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^-+|-+$/g, '');
// Sicherer Basisname ohne Endung.
function safeBase(raw) {
const base = path.basename(String(raw || 'bild')).replace(/\.[^.]+$/, '');
const cleaned = base.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
return cleaned || 'bild';
}
// Kurze eindeutige Endung, damit gleichnamige Uploads nicht kollidieren.
function uid() {
return Date.now().toString(36).slice(-4) + Math.random().toString(36).slice(2, 5);
}
export default upload;