ci / build-and-test (push) Canceled after 0s
scripts/build-og-image.ts (renamed from .mjs, run via tsx) now also queries getAvisosAmbientales and rasterizes a 1200x630 PNG per aviso at public/og/avis/[origen]-[id].png. The SVG is built inline from the aviso fields: title (truncated to 110 chars), summary (200), source and municipio as meta line. [slug].astro passes /og/avis/<slug>.png + the title as alt to BaseLayout, which emits og:image, og:image:alt, twitter:image and twitter:image:alt automatically. e2e: detail page test now also asserts og:image points at the per-aviso PNG and that the PNG returns 200. 20/20 e2e pass; check and eslint clean.
75 lines
3.4 KiB
TypeScript
75 lines
3.4 KiB
TypeScript
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
import { dirname, resolve, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import sharp from 'sharp';
|
|
import { getAvisosAmbientales } from '../src/lib/db';
|
|
import type { Aviso } from '../src/lib/types';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(here, '..');
|
|
const publicDir = resolve(repoRoot, 'public');
|
|
const defaultSvgPath = resolve(publicDir, 'og-default.svg');
|
|
const defaultPngPath = resolve(publicDir, 'og-default.png');
|
|
const perAvisoDir = resolve(publicDir, 'og/avis');
|
|
|
|
const escapeXml = (s: string): string =>
|
|
s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
const fitText = (text: string, maxChars: number): string =>
|
|
text.length <= maxChars ? text : `${text.slice(0, maxChars - 1).trimEnd()}…`;
|
|
|
|
const renderAvisoSvg = (aviso: Aviso): string => {
|
|
const titulo = fitText(escapeXml(aviso.titulo), 110);
|
|
const resumen = fitText(escapeXml(aviso.resumen), 200);
|
|
const meta = [aviso.fuente, aviso.municipio]
|
|
.filter((s): s is string => Boolean(s))
|
|
.map(escapeXml)
|
|
.join(' · ');
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
<title>${escapeXml(aviso.titulo)}</title>
|
|
<rect width="1200" height="630" fill="#efeae0"/>
|
|
<rect x="54" y="54" width="1092" height="522" fill="#f8f5ee" stroke="#1f2a24" stroke-width="3"/>
|
|
<path d="M118 190c54-132 118-184 300-212-91 61-137 136-164 267-45-15-91-26-136-55z" fill="#2f6e51" transform="translate(0 96)"/>
|
|
<text x="118" y="354" fill="#1f2a24" font-family="Georgia, serif" font-size="56" font-weight="700">${titulo}</text>
|
|
<line x1="118" y1="378" x2="1082" y2="378" stroke="#d7cfbd" stroke-width="2"/>
|
|
<text x="118" y="428" fill="#4a5751" font-family="Georgia, serif" font-size="26">${resumen}</text>
|
|
<line x1="118" y1="528" x2="1082" y2="528" stroke="#d7cfbd" stroke-width="2"/>
|
|
<text x="118" y="556" fill="#1d4a37" font-family="Arial, sans-serif" font-size="20" font-weight="700">${meta}</text>
|
|
<text x="1082" y="556" text-anchor="end" fill="#4a5751" font-family="Arial, sans-serif" font-size="20">Ibiza Environmental Watch</text>
|
|
</svg>
|
|
`;
|
|
};
|
|
|
|
const rasterizeSvg = async (svg: string, outPath: string): Promise<number> => {
|
|
const png = await sharp(Buffer.from(svg), { density: 192 })
|
|
.resize({ width: 1200, height: 630, fit: 'contain', background: { r: 239, g: 234, b: 224, alpha: 1 } })
|
|
.png({ compressionLevel: 9 })
|
|
.toBuffer();
|
|
await writeFile(outPath, png);
|
|
return png.length;
|
|
};
|
|
|
|
const defaultSvg = await readFile(defaultSvgPath);
|
|
await mkdir(dirname(defaultPngPath), { recursive: true });
|
|
const defaultBytes = await sharp(defaultSvg, { density: 192 })
|
|
.resize({ width: 1200, height: 630, fit: 'contain', background: { r: 239, g: 234, b: 224, alpha: 1 } })
|
|
.png({ compressionLevel: 9 })
|
|
.toBuffer();
|
|
await writeFile(defaultPngPath, defaultBytes);
|
|
console.log(`generated ${defaultPngPath} (${defaultBytes.length} bytes)`);
|
|
|
|
const avisos = await getAvisosAmbientales();
|
|
await mkdir(perAvisoDir, { recursive: true });
|
|
for (const aviso of avisos) {
|
|
const slug = `${aviso.origen}-${aviso.id}`;
|
|
const out = join(perAvisoDir, `${slug}.png`);
|
|
const bytes = await rasterizeSvg(renderAvisoSvg(aviso), out);
|
|
console.log(`generated ${out} (${bytes} bytes)`);
|
|
}
|