Files
itziarZG 0abbd77828
ci / build-and-test (push) Canceled after 0s
feat(seo): per-aviso OG images generated at build
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.
2026-07-26 22:15:52 +02:00

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
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)`);
}