The page now leads with the actual measurement (A+ rating from Website
Carbon Calculator: cleaner than 97% of tested sites, 0.01g CO2/visit,
~1.78 kg CO2/year at 10k visits/month — equivalent to boiling water
for 241 cups of tea) instead of the 'to be completed once deployed'
placeholder. Two micro-adjustments to keep repo conventions: link
uses rel=noopener noreferrer and includes the standard SR cue for
new-tab links, matching the rest of the site.
Replace the old 'Sense trackers' placeholder (which said we'd use
Plausible or Umami in the future) with an honest description of the
current setup: self-hosted Umami at stats.itziarzameza.dev, counting
aggregate pageviews and anonymous sessions only — no cookies, no
fingerprinting, no personal data, no third-party sharing. Explicit
'never Google Analytics' to keep the privacy promise visible.
Add the Umami analytics tracker (privacy-friendly, no cookies, GDPR
compliant by default) to BaseLayout. Two perf-conscious choices:
- script is:inline defer — Astro emits it as-is without bundling,
and defer keeps it off the critical render path
- preconnect to https://stats.itziarzameza.dev — DNS/TCP/TLS
handshake happens in parallel with other resources, so the actual
script fetch is amortized
Website ID a2067ef9-b05a-4147-9a18-31743d615716 is hardcoded as a
data attribute; rotate via a build env var if it ever needs to
change across environments.
The map's marker loop used municipioCoords[municipio] with an exact
key match. If the DB returned a short form ('Sant Antoni') or a
Spanish variant ('San Antonio', 'Ibiza', 'Santa Eulalia'), the
lookup silently failed — the municipio still appeared in the text
summary (which uses raw DB values) but not as a map marker.
Replace the direct dict access with a getCoordsForMunicipio helper that:
1. Tries exact match first (fast path)
2. Falls back to an explicit alias map (short forms + Spanish variants
in both accented and unaccented variants)
3. Finally falls back to a normalized (case- + accent-insensitive)
prefix match in either direction
Aliases cover: Ibiza, San Antonio, San Juan, San José, Santa Eulalia,
and their accented/longer forms. Municipality without a real
coordinate ('Municipi no especificat') still returns undefined and
is silently skipped, as intended.
astro check now runs in prebuild and rejects fixtures that don't match
the Aviso interface. After adding categoria to the interface, fixtures
need it too — even though they're only used as fallback when
DATABASE_URL is unset, they still get type-checked.
The Tema dropdown previously listed every unique value from the IA-generated
temas array, producing 100+ pills with duplicates like 'desarrollo sostenible'
vs 'desarrollo_sostenible', 'biodiversidad' vs 'biodiversidad_marina', etc.
Switch the Tema filter source from aviso.temas to the new aviso.categoria
column (13 fixed values). The descriptive temas tags continue to render
inside each card unchanged.
- types.ts: add categoria: string | null to Aviso
- db.ts: SELECT categoria in both queries, map to Aviso
- index.astro: compute categorias from aviso.categoria, pass to Filters
- Filters.astro: rename prop to categorias, switch data-filter-type to
'categoria', read data-categoria (single value, exact match) instead of
data-temas (pipe-split, includes)
- EntryCard.astro: emit data-categoria instead of data-temas
The temas array is still rendered inside each card via aviso.temas.map().
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.
Coolify builds were failing with ERR_PNPM_OUTDATED_LOCKFILE because
the devDependencies added in recent commits (eslint, playwright,
@astrojs/check, tsx, sharp, etc.) had been installed with npm locally
and the pnpm-lock.yaml in the repo was stale.
- Regenerate pnpm-lock.yaml from current package.json.
- Promote sharp to a direct devDependency (scripts/build-og-image.ts
imports it; pnpm cannot resolve transitive packages outside the
importer graph).
- Pin esbuild and sharp as allowed build scripts in pnpm-workspace.yaml
so pnpm 11's default deny policy does not skip their native postinstall
on a clean install. Both are required: esbuild for the Vite/Astro
binary, sharp for the OG image rasterizer.
- Add a pnpm engine pin (>=9) so installs that target the new schema
fail fast instead of silently downgrading.
Verified locally: pnpm install (clean), pnpm run prebuild (regenerates
OG images, astro check, eslint), pnpm run build and pnpm run test:e2e
all pass (20/20).
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.
Generate a static page per aviso at /avis/[origen]-[id] using getStaticPaths
from getAvisosAmbientales. Each page carries a NewsArticle JSON-LD node
(headline, description, datePublished, inLanguage, isAccessibleForFree,
keywords, author, publisher, mainEntityOfPage) and renders the title,
metadata, temas, summary and a 'font original' aside pointing at the
external source (rel=noopener noreferrer, target=_blank, with a
visually-hidden SR cue). EntryCard keeps the external link as the primary
action and gains an inline 'Llegir més' link to the internal detail.
e2e coverage:
- detail page renders H1 and external link
- JSON-LD parses and includes Organization, WebSite and NewsArticle with
the expected metadata
- every URL listed in sitemap-0.xml under /avis/ returns 200
Sitemap now lists the 6 fixture avisos as canonical URLs. JSON-LD
selectors are scoped to head to avoid inline scripts in the body.
20/20 e2e pass; astro check and eslint clean.
Pin eslint@9 (jsx-a11y peer) and add eslint-plugin-astro + eslint-plugin-jsx-a11y + typescript-eslint as devDeps.
eslint.config.js wires:
- @eslint/js recommended and typescript-eslint recommended for general .ts/.mjs.
- astro flat/jsx-a11y-recommended for .astro files, registering the jsx-a11y plugin explicitly.
- project-specific tweaks: drop jsx-a11y/anchor-is-valid (noisy on Astro links), warn on click-events-have-key-events and no-static-element-interactions, allow any in scripts/tests, ignore env.d.ts triple-slash ref (Astro convention).
Scripts: lint and lint:fix. prebuild now also runs lint, so the same chain guards a11y regressions as type and build errors.
CI workflow gains a lint step between check and build. 0 errors, 16/16 e2e still pass.
Run on push and pull_request to main. Steps:
- pnpm install --frozen-lockfile with store cache keyed on pnpm-lock.yaml.
- Playwright chromium install.
- pnpm run check (typecheck via astro check).
- pnpm run build (prebuild regenerates og-default.png and re-runs check).
- Upload dist/ as a build artifact.
- pnpm run test:e2e (pretest:e2e re-runs check + build so the preview server is always fresh).
- Upload playwright-report on every run for post-mortem.
Concurrency group cancels in-progress runs on the same ref.
Make typecheck part of the local guard rails without new tooling:
- prebuild now runs the OG image rasterizer and astro check before astro build.
- pretest:e2e runs astro check and astro build so playwright always exercises a fresh, type-checked dist.
> ibiza-environmental-watch@0.1.0 prebuild
> node scripts/build-og-image.mjs && astro check
generated /home/izg/REPOS/EivissaEnvirnmentalWatch/public/og-default.png (34393 bytes)
21:39:47 [types] Generated 169ms
21:39:47 [check] Getting diagnostics for Astro files in /home/izg/REPOS/EivissaEnvirnmentalWatch...
Result (18 files):
- 0 errors
- 0 warnings
- 0 hints
> ibiza-environmental-watch@0.1.0 build
> astro build
21:40:03 [types] Generated 151ms
21:40:03 [build] output: "static"
21:40:03 [build] mode: "static"
21:40:03 [build] directory: /home/izg/REPOS/EivissaEnvirnmentalWatch/dist/
21:40:03 [build] Collecting build info...
21:40:03 [build] ✓ Completed in 331ms.
21:40:03 [build] Building static entrypoints...
21:40:04 [vite] ✓ built in 1.00s
21:40:04 [vite] ✓ built in 356ms
21:40:04 [build] Rearranging server assets...
generating static routes
21:40:04 ├─ /404.html (+43ms)
21:40:04 ├─ /huella/index.html (+6ms)
21:40:04 ├─ /robots.txt (+4ms)
21:40:04 ├─ /rss.xml (+114ms)
21:40:04 ├─ /index.html (+32ms)
21:40:04 ✓ Completed in 247ms.
21:40:04 [build] ✓ Completed in 1.74s.
21:40:04 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
21:40:04 [build] 3 page(s) built in 2.11s
21:40:04 [build] Complete! and
> ibiza-environmental-watch@0.1.0 pretest:e2e
> astro check && astro build
21:40:09 [types] Generated 149ms
21:40:09 [check] Getting diagnostics for Astro files in /home/izg/REPOS/EivissaEnvirnmentalWatch...
Result (18 files):
- 0 errors
- 0 warnings
- 0 hints
21:40:24 [types] Generated 101ms
21:40:24 [build] output: "static"
21:40:24 [build] mode: "static"
21:40:24 [build] directory: /home/izg/REPOS/EivissaEnvirnmentalWatch/dist/
21:40:24 [build] Collecting build info...
21:40:24 [build] ✓ Completed in 330ms.
21:40:24 [build] Building static entrypoints...
21:40:26 [vite] ✓ built in 1.48s
21:40:26 [vite] ✓ built in 380ms
21:40:26 [build] Rearranging server assets...
generating static routes
21:40:26 ├─ /404.html (+81ms)
21:40:26 ├─ /huella/index.html (+14ms)
21:40:26 ├─ /robots.txt (+9ms)
21:40:26 ├─ /rss.xml (+134ms)
21:40:27 ├─ /index.html (+32ms)
21:40:27 ✓ Completed in 322ms.
21:40:27 [build] ✓ Completed in 2.30s.
21:40:27 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
21:40:27 [build] 3 page(s) built in 2.66s
21:40:27 [build] Complete!
> ibiza-environmental-watch@0.1.0 test:e2e
> playwright test
Running 16 tests using 6 workers
✓ 6 [desktop] › tests/e2e/site.spec.ts:71:3 › site smoke › robots.txt built from Astro.site and points at the sitemap (359ms)
✓ 7 [desktop] › tests/e2e/site.spec.ts:79:3 › site smoke › sitemap lists the two real pages and not the 404 (619ms)
✓ 8 [desktop] › tests/e2e/site.spec.ts:92:3 › build artifacts › og PNG exists with the right dimensions (73ms)
✓ 2 [desktop] › tests/e2e/site.spec.ts:54:3 › site smoke › 404 page is noindex and reachable (2.1s)
✓ 1 [desktop] › tests/e2e/site.spec.ts:62:3 › site smoke › JSON-LD on the home parses and exposes Organization + ItemList (3.3s)
✓ 4 [desktop] › tests/e2e/site.spec.ts:24:3 › site smoke › skip-link is the first interactive element and jumps to main (4.2s)
✓ 5 [desktop] › tests/e2e/site.spec.ts:33:3 › site smoke › filter pills toggle aria-pressed and update the live counter (4.4s)
✓ 10 [mobile] › tests/e2e/site.spec.ts:24:3 › site smoke › skip-link is the first interactive element and jumps to main (2.8s)
✓ 14 [mobile] › tests/e2e/site.spec.ts:71:3 › site smoke › robots.txt built from Astro.site and points at the sitemap (274ms)
✓ 15 [mobile] › tests/e2e/site.spec.ts:79:3 › site smoke › sitemap lists the two real pages and not the 404 (538ms)
✓ 16 [mobile] › tests/e2e/site.spec.ts:92:3 › build artifacts › og PNG exists with the right dimensions (50ms)
✓ 11 [mobile] › tests/e2e/site.spec.ts:33:3 › site smoke › filter pills toggle aria-pressed and update the live counter (3.1s)
✓ 13 [mobile] › tests/e2e/site.spec.ts:62:3 › site smoke › JSON-LD on the home parses and exposes Organization + ItemList (1.9s)
✓ 12 [mobile] › tests/e2e/site.spec.ts:54:3 › site smoke › 404 page is noindex and reachable (1.9s)
✓ 3 [desktop] › tests/e2e/site.spec.ts:9:3 › site smoke › home renders and reports no axe violations @ desktop (12.7s)
✓ 9 [mobile] › tests/e2e/site.spec.ts:9:3 › site smoke › home renders and reports no axe violations @ desktop (7.5s)
16 passed (23.4s) therefore catch type and a11y regressions before code ships, mirroring what a precommit hook would do.
Add @playwright/test, axe-core and @axe-core/playwright as devDependencies.
- playwright.config.ts with desktop+mobile projects and a webServer that boots astro preview on 127.0.0.1:4321.
- tests/e2e/site.spec.ts covers: home renders with H1 and textual map summary; skip-link is the first focusable and jumps to main; filter pills toggle aria-pressed and update the live counter; 404 is noindex; JSON-LD parses and exposes Organization+WebSite+ItemList; robots.txt and sitemap are wired to Astro.site; the 404 is excluded from the sitemap; the generated og-default.png is 1200x630.
- Scripts: test:e2e runs the suite, test:e2e:install pulls the chromium headless shell.
- .gitignore ignores test-results/, playwright-report/ and the playwright cache.
All 16 tests pass (8 desktop, 8 mobile).
Add scripts/build-og-image.mjs (sharp) hooked as npm prebuild to materialize a 1200x630 PNG alongside the SVG. BaseLayout now points og:image/twitter:image at the PNG, which the major crawlers preview reliably (SVG support is patchy). The SVG stays in the repo as the source of truth.
Replace the hardcoded public/robots.txt with src/pages/robots.txt.ts so the Sitemap URL always matches the configured site (no drift between the file and astro.config.mjs).
Add npm run check and check:watch and pin @astrojs/check + typescript in devDependencies so the existing strict tsconfig actually runs. Current build is clean (0 errors).
SEO
- BaseLayout: canonical, hreflang, author, extended robots, full OG and Twitter tags with default social image.
- Add JSON-LD @graph: Organization + WebSite + page-specific type; homepage adds ItemList with one Article per aviso.
- Add public/og-default.svg (1200x630) used as fallback social card.
- New src/pages/404.astro with noindex meta.
- Filters now use fieldset+legend and aria-controls; counter is role=status with initial SSR value; noscript fallback included.
- Filter pills taller (min-height 2.25rem) and switch from style.display to the hidden attribute.
Accessibility
- Map markers become keyboard accessible (role=button, tabindex, aria-label, aria-keyshortcuts); opening a popup moves focus to its first link and returns focus on close. Map container upgraded to role=region with aria-describedby.
- Page introduces a textual per-municipio summary as the accessible equivalent of the map.
- Focus-visible outline extended to summary and [tabindex]; outline contrast tuned for active filter pills and Leaflet interactive layer.
- EntryCard exposes id+aria-labelledby; tema list now has aria-label; stamp color tightened for AA contrast.
- BaseLayout: header gains an aria-labelled nav with aria-current; main gets a focus ring when targeted by the skip link.
- Markers read their colors from CSS custom properties instead of hardcoded hex.
External links now consistently:
- Use rel=noopener noreferrer (privacy: don't leak referrer)
- Include a visually-hidden span announcing 's'obre en una pestanya nova'
so screen reader users know the link opens in a new tab
Updated: EntryCard title link, huella.astro (Website Carbon, EcoGrader,
Green Web Foundation). BaseLayout footer links updated in previous
commit.
- Wrap each .filters__pills in role=group with aria-labelledby pointing
to the label
- Add type=button to all pill buttons
- Toggle aria-pressed alongside .is-active class on click
- All inactive pills start with aria-pressed=false
The page previously jumped from masthead (a <a>, not a heading) directly
to <h2> inside EntryCard, leaving the page without a primary heading.
Add a visible h1 introducing the feed: 'Avisos ambientals d'Eivissa
i Formentera'.
- Add canonical, Open Graph, Twitter Card, robots, theme-color to <head>
- Add preconnect to CARTO CDN for faster map tile paint
- Add alternate RSS link
- Add skip link as first body child
- Add id+tabindex to <main> so skip link works
- Add aria-current=page to brand link on homepage
- Update external links to rel=noopener noreferrer with screen-reader cue
- Keep brand as styled <a> (not <h1>) — page-level h1 added in pages
Dynamic await import of CSS only emits a JS shim (leaflet-src.HASH.js)
that injects styles at runtime, but the actual stylesheet file
(leaflet.HASH.css) is never written to dist/_astro/, causing 404s
on Astro's preload helper.
Static top-level import is processed at build time and emits the CSS.
Lazy-loading is preserved: the component still hydrates only with
client:media="(min-width: 900px)" in index.astro.