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 ` + + ${escapeXml(aviso.titulo)} + + + + ${titulo} + + ${resumen} + + ${meta} + Ibiza Environmental Watch + +`; +}; + +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 }) => {