diff --git a/package-lock.json b/package-lock.json
index fb96469..c77fd9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"eslint": "^9.39.5",
"eslint-plugin-astro": "^3.0.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
+ "tsx": "^4.23.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.65.0"
},
@@ -932,18 +933,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@emnapi/core": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
- "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.3",
- "tslib": "^2.4.0"
- }
- },
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
@@ -954,17 +943,6 @@
"tslib": "^2.4.0"
}
},
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
- "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
@@ -8271,6 +8249,40 @@
"license": "0BSD",
"optional": true
},
+ "node_modules/tsx": {
+ "version": "4.23.1",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
+ "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -9331,7 +9343,7 @@
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
diff --git a/package.json b/package.json
index dd68ed9..e7be0f0 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
},
"scripts": {
"dev": "astro dev",
- "prebuild": "node scripts/build-og-image.mjs && astro check && eslint .",
+ "prebuild": "tsx scripts/build-og-image.ts && astro check && eslint .",
"build": "astro build",
"preview": "astro preview",
"check": "astro check",
@@ -39,6 +39,7 @@
"eslint": "^9.39.5",
"eslint-plugin-astro": "^3.0.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
+ "tsx": "^4.23.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.65.0"
}
diff --git a/public/og/avis/boib-1.png b/public/og/avis/boib-1.png
new file mode 100644
index 0000000..2dbdb26
Binary files /dev/null and b/public/og/avis/boib-1.png differ
diff --git a/public/og/avis/boib-4.png b/public/og/avis/boib-4.png
new file mode 100644
index 0000000..a1d487d
Binary files /dev/null and b/public/og/avis/boib-4.png differ
diff --git a/public/og/avis/prensa-2.png b/public/og/avis/prensa-2.png
new file mode 100644
index 0000000..5aa8275
Binary files /dev/null and b/public/og/avis/prensa-2.png differ
diff --git a/public/og/avis/prensa-3.png b/public/og/avis/prensa-3.png
new file mode 100644
index 0000000..b79825b
Binary files /dev/null and b/public/og/avis/prensa-3.png differ
diff --git a/public/og/avis/prensa-5.png b/public/og/avis/prensa-5.png
new file mode 100644
index 0000000..c9c092c
Binary files /dev/null and b/public/og/avis/prensa-5.png differ
diff --git a/public/og/avis/prensa-6.png b/public/og/avis/prensa-6.png
new file mode 100644
index 0000000..f834f64
Binary files /dev/null and b/public/og/avis/prensa-6.png differ
diff --git a/scripts/build-og-image.mjs b/scripts/build-og-image.mjs
deleted file mode 100644
index de5da35..0000000
--- a/scripts/build-og-image.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-import { readFile, writeFile, mkdir } from 'node:fs/promises';
-import { dirname, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import sharp from 'sharp';
-
-const here = dirname(fileURLToPath(import.meta.url));
-const repoRoot = resolve(here, '..');
-const svgPath = resolve(repoRoot, 'public/og-default.svg');
-const pngPath = resolve(repoRoot, 'public/og-default.png');
-
-const svg = await readFile(svgPath);
-const png = await sharp(svg, { density: 192 })
- .resize({ width: 1200, height: 630, fit: 'contain', background: { r: 239, g: 234, b: 224, alpha: 1 } })
- .png({ compressionLevel: 9 })
- .toBuffer();
-
-await mkdir(dirname(pngPath), { recursive: true });
-await writeFile(pngPath, png);
-console.log(`generated ${pngPath} (${png.length} bytes)`);
diff --git a/scripts/build-og-image.ts b/scripts/build-og-image.ts
new file mode 100644
index 0000000..fb5d7ed
--- /dev/null
+++ b/scripts/build-og-image.ts
@@ -0,0 +1,74 @@
+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, ''');
+
+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 `
+
+`;
+};
+
+const rasterizeSvg = async (svg: string, outPath: string): Promise => {
+ 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)`);
+}
diff --git a/src/pages/avis/[slug].astro b/src/pages/avis/[slug].astro
index 8a2d734..7133754 100644
--- a/src/pages/avis/[slug].astro
+++ b/src/pages/avis/[slug].astro
@@ -47,6 +47,8 @@ const articleSchema = {
{
expect(article?.['isAccessibleForFree']).toBe(true);
expect(article?.['inLanguage']).toBe('ca-ES');
expect(article?.['mainEntityOfPage']).toBeTruthy();
+
+ const ogImage = await page.locator('meta[property="og:image"]').first().getAttribute('content');
+ expect(ogImage).toMatch(/\/og\/avis\/[a-z0-9-]+\.png$/);
+ const ogUrl = ogImage!.replace('https://www.eivissaenvironmentalwatch.es', '');
+ const ogRes = await request.get(ogUrl);
+ expect(ogRes.status(), ogUrl).toBe(200);
});
test('sitemap includes every aviso detail page', async ({ request }) => {