From a06316dadea93f3f6e8a3312b594ba7f4722cfdf Mon Sep 17 00:00:00 2001 From: itziarZG Date: Sun, 26 Jul 2026 22:27:32 +0200 Subject: [PATCH] feat(seo): custom news sitemap with per-aviso news blocks Replace the @astrojs/sitemap integration with two handwritten endpoints under src/pages/ so we can attach a Google News block to each aviso detail URL. The integration already emitted the news namespace but never the per-URL blocks, so the project was not eligible for Google News indexing. - src/pages/sitemap-index.xml.ts: single-child sitemapindex pointing at /sitemap-0.xml. - src/pages/sitemap-0.xml.ts: home + huella + one entry per aviso from getAvisosAmbientales. Each aviso carries with publication name (fuente), language ca, publication_date (ISO), title and keywords (temas joined). - astro.config.mjs: drop the @astrojs/sitemap integration; remove the dependency from package.json and pnpm-lock.yaml. - e2e: add a test asserting the news namespace and at least one block with publication_date and language. 22/22 e2e pass; check and eslint clean. --- astro.config.mjs | 3 +- package.json | 1 - pnpm-lock.yaml | 53 -------------------------------- src/pages/sitemap-0.xml.ts | 55 ++++++++++++++++++++++++++++++++++ src/pages/sitemap-index.xml.ts | 15 ++++++++++ tests/e2e/site.spec.ts | 10 +++++++ 6 files changed, 81 insertions(+), 56 deletions(-) create mode 100644 src/pages/sitemap-0.xml.ts create mode 100644 src/pages/sitemap-index.xml.ts diff --git a/astro.config.mjs b/astro.config.mjs index d5d014e..2317744 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,6 +1,5 @@ import { defineConfig } from 'astro/config'; import preact from '@astrojs/preact'; -import sitemap from '@astrojs/sitemap'; // Salida estática pura: HTML/JSON servidos por Caddy, sin proceso Node en // producción. Los datos se consultan a Postgres una única vez, en build-time @@ -8,7 +7,7 @@ import sitemap from '@astrojs/sitemap'; export default defineConfig({ output: 'static', site: 'https://www.eivissaenvironmentalwatch.es', - integrations: [preact(), sitemap()], + integrations: [preact()], build: { inlineStylesheets: 'auto', // inline CSS pequeños, externos los grandes (ej. Leaflet) }, diff --git a/package.json b/package.json index 6918149..e8d4e2c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "dependencies": { "@astrojs/preact": "^6.0.1", "@astrojs/rss": "^4.0.19", - "@astrojs/sitemap": "^3.7.3", "astro": "^7.1.3", "leaflet": "^1.9.4", "pg": "^8.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecb393f..5e266bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@astrojs/rss': specifier: ^4.0.19 version: 4.0.19 - '@astrojs/sitemap': - specifier: ^3.7.3 - version: 3.7.3 astro: specifier: ^7.1.3 version: 7.1.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(tsx@4.23.1)(yaml@2.9.0) @@ -180,9 +177,6 @@ packages: '@astrojs/rss@4.0.19': resolution: {integrity: sha512-e+z5wYeYtffQdHQO8c2tkSd2JEBdAuRXJV4ZEU5IxkYeE6e39woDd7nw1PH1Kk2tEYNCYuKdylnnbhGmt61awA==} - '@astrojs/sitemap@3.7.3': - resolution: {integrity: sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==} - '@astrojs/telemetry@3.3.3': resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -1293,18 +1287,12 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node@24.13.3': - resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - '@types/node@26.1.1': resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/sax@1.2.7': - resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1444,9 +1432,6 @@ packages: anynum@1.0.1: resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2824,11 +2809,6 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - sitemap@9.0.1: - resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==} - engines: {node: '>=20.19.5', npm: '>=10.8.2'} - hasBin: true - smol-toml@1.7.0: resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} engines: {node: '>= 18'} @@ -2856,9 +2836,6 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stream-replace-string@2.0.0: - resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2988,9 +2965,6 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -3498,12 +3472,6 @@ snapshots: piccolore: 0.1.3 zod: 4.4.3 - '@astrojs/sitemap@3.7.3': - dependencies: - sitemap: 9.0.1 - stream-replace-string: 2.0.0 - zod: 4.4.3 - '@astrojs/telemetry@3.3.3': dependencies: ci-info: 4.4.0 @@ -4385,10 +4353,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@24.13.3': - dependencies: - undici-types: 7.18.2 - '@types/node@26.1.1': dependencies: undici-types: 8.3.0 @@ -4399,10 +4363,6 @@ snapshots: pg-protocol: 1.15.0 pg-types: 2.2.0 - '@types/sax@1.2.7': - dependencies: - '@types/node': 26.1.1 - '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': @@ -4593,8 +4553,6 @@ snapshots: anynum@1.0.1: {} - arg@5.0.2: {} - argparse@2.0.1: {} aria-query@5.3.2: {} @@ -6289,13 +6247,6 @@ snapshots: sisteransi@1.0.5: {} - sitemap@9.0.1: - dependencies: - '@types/node': 24.13.3 - '@types/sax': 1.2.7 - arg: 5.0.2 - sax: 1.6.0 - smol-toml@1.7.0: {} source-map-js@1.2.1: {} @@ -6313,8 +6264,6 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stream-replace-string@2.0.0: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6477,8 +6426,6 @@ snapshots: uncrypto@0.1.3: {} - undici-types@7.18.2: {} - undici-types@8.3.0: {} unified@11.0.5: diff --git a/src/pages/sitemap-0.xml.ts b/src/pages/sitemap-0.xml.ts new file mode 100644 index 0000000..127efa4 --- /dev/null +++ b/src/pages/sitemap-0.xml.ts @@ -0,0 +1,55 @@ +import type { APIContext } from 'astro'; +import { getAvisosAmbientales } from '../lib/db'; + +const escapeXml = (s: string): string => + s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +const STATIC_PATHS = ['/', '/huella/'] as const; + +export async function GET(context: APIContext) { + const site = (context.site ?? new URL(context.url.origin)).toString().replace(/\/$/, ''); + const avisos = await getAvisosAmbientales(); + + const staticUrls = STATIC_PATHS.map( + (p) => ` + ${site}${p} + ` + ).join('\n'); + + const avisoUrls = avisos + .map((aviso) => { + const path = `/avis/${aviso.origen}-${aviso.id}/`; + const title = escapeXml(aviso.titulo); + const keywords = escapeXml(aviso.temas.join(', ')); + const pubDate = new Date(aviso.fecha).toISOString(); + return ` + ${site}${path} + + + ${escapeXml(aviso.fuente)} + ca + + ${pubDate} + ${title} + ${keywords} + + `; + }) + .join('\n'); + + const body = ` + +${staticUrls} +${avisoUrls} + +`; + + return new Response(body, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }); +} diff --git a/src/pages/sitemap-index.xml.ts b/src/pages/sitemap-index.xml.ts new file mode 100644 index 0000000..c04c1e0 --- /dev/null +++ b/src/pages/sitemap-index.xml.ts @@ -0,0 +1,15 @@ +import type { APIContext } from 'astro'; + +export function GET(context: APIContext) { + const site = (context.site ?? new URL(context.url.origin)).toString().replace(/\/$/, ''); + const body = ` + + + ${site}/sitemap-0.xml + + +`; + return new Response(body, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }); +} diff --git a/tests/e2e/site.spec.ts b/tests/e2e/site.spec.ts index 8312a7e..e29920d 100644 --- a/tests/e2e/site.spec.ts +++ b/tests/e2e/site.spec.ts @@ -129,6 +129,16 @@ test.describe('site smoke', () => { expect(r.status(), p).toBe(200); } }); + + test('news sitemap declares Google News namespace and per-aviso news block', async ({ request }) => { + const res = await request.get('/sitemap-0.xml'); + const sitemap = await res.text(); + expect(sitemap).toContain('xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"'); + const newsBlocks = sitemap.match(//g) ?? []; + expect(newsBlocks.length).toBeGreaterThan(0); + expect(sitemap).toMatch(/[^<]+<\/news:publication_date>/); + expect(sitemap).toMatch(/ca<\/news:language>/); + }); }); test.describe('build artifacts', () => {