feat(seo): custom news sitemap with per-aviso news blocks
ci / build-and-test (push) Canceled after 0s
ci / build-and-test (push) Canceled after 0s
Replace the @astrojs/sitemap integration with two handwritten endpoints under src/pages/ so we can attach a Google News <news: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 <news:news> 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 <news:news> block with publication_date and language. 22/22 e2e pass; check and eslint clean.
This commit is contained in:
+1
-2
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
-53
@@ -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:
|
||||
|
||||
@@ -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, '"')
|
||||
.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) => ` <url>
|
||||
<loc>${site}${p}</loc>
|
||||
</url>`
|
||||
).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 ` <url>
|
||||
<loc>${site}${path}</loc>
|
||||
<news:news>
|
||||
<news:publication>
|
||||
<news:name>${escapeXml(aviso.fuente)}</news:name>
|
||||
<news:language>ca</news:language>
|
||||
</news:publication>
|
||||
<news:publication_date>${pubDate}</news:publication_date>
|
||||
<news:title>${title}</news:title>
|
||||
<news:keywords>${keywords}</news:keywords>
|
||||
</news:news>
|
||||
</url>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
${staticUrls}
|
||||
${avisoUrls}
|
||||
</urlset>
|
||||
`;
|
||||
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>${site}/sitemap-0.xml</loc>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
||||
`;
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
@@ -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(/<news:news>/g) ?? [];
|
||||
expect(newsBlocks.length).toBeGreaterThan(0);
|
||||
expect(sitemap).toMatch(/<news:publication_date>[^<]+<\/news:publication_date>/);
|
||||
expect(sitemap).toMatch(/<news:language>ca<\/news:language>/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('build artifacts', () => {
|
||||
|
||||
Reference in New Issue
Block a user