32 Commits
Author SHA1 Message Date
itziarZG e9d058cdb6 docs(huella): replace placeholder with real Website Carbon A+ rating
ci / build-and-test (push) Canceled after 0s
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.
2026-07-28 15:06:37 +02:00
itziarZG b136b9e540 docs(huella): document Umami analytics now in use
ci / build-and-test (push) Canceled after 0s
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.
2026-07-27 15:13:55 +02:00
itziarZG 328c0331a2 feat(analytics): add Umami tracking script with preconnect
ci / build-and-test (push) Canceled after 0s
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.
2026-07-27 15:08:09 +02:00
itziarZG 501a1d602c fix(map): tolerant municipio lookup handles short forms, Spanish variants, accents
ci / build-and-test (push) Canceled after 0s
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.
2026-07-27 12:55:14 +02:00
itziarZG 4866681a9a fix(fixtures): add categoria field to satisfy Aviso type
ci / build-and-test (push) Canceled after 0s
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.
2026-07-27 12:04:13 +02:00
itziarZG 4013cb8365 feat(filters): use categoria (closed set, 13 values) instead of temas for 'Tema' filter
ci / build-and-test (push) Canceled after 0s
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().
2026-07-27 10:06:47 +02:00
itziarZG a06316dade feat(seo): custom news sitemap with per-aviso news blocks
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.
2026-07-26 22:27:32 +02:00
itziarZG f61cde82f9 build: refresh pnpm lockfile and pin build approvals
ci / build-and-test (push) Canceled after 0s
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).
2026-07-26 22:20:38 +02:00
itziarZG 0abbd77828 feat(seo): per-aviso OG images generated at build
ci / build-and-test (push) Canceled after 0s
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.
2026-07-26 22:15:52 +02:00
itziarZG 743bbbb495 feat: per-aviso detail pages with NewsArticle JSON-LD
ci / build-and-test (push) Canceled after 0s
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.
2026-07-26 22:10:20 +02:00
itziarZG 813246c211 ci: add ESLint flat config with astro + jsx-a11y
ci / build-and-test (push) Canceled after 0s
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.
2026-07-26 21:50:42 +02:00
itziarZG e0b812b10d ci: add Gitea Actions workflow (check + build + e2e)
ci / build-and-test (push) Canceled after 0s
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.
2026-07-26 21:44:32 +02:00
itziarZG 6f4250d112 chore: gate build and e2e on astro check
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.
2026-07-26 21:40:52 +02:00
itziarZG c492e5e214 test(e2e): add Playwright + axe-core smoke suite
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).
2026-07-26 21:37:00 +02:00
itziarZG f5eac75887 feat(seo): rasterize og-default.png from SVG during build
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.
2026-07-26 21:28:53 +02:00
itziarZG f4be17ccf5 fix(seo): generate robots.txt from Astro.site at build
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).
2026-07-26 21:28:03 +02:00
itziarZG 1d380d5cb5 chore: add astro check script with typecheck devDeps
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).
2026-07-26 21:27:41 +02:00
itziarZG 9d8338cbef feat(seo+a11y): JSON-LD/OG, accessible filters/map, 404 and social card
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.
2026-07-26 21:24:54 +02:00
itziarZG a09543512d fix(a11y): rel noopener noreferrer + SR cue on all external links
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.
2026-07-25 19:29:43 +02:00
itziarZG 592412fec7 feat(a11y): add aria-pressed and group semantics to filter pills
- 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
2026-07-25 19:29:33 +02:00
itziarZG 1b5ffe6a7a fix(seo): add h1 to homepage feed section
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'.
2026-07-25 19:29:21 +02:00
itziarZG 14600e9406 feat(seo+a11y): canonical/OG/Twitter meta, skip link, aria-current, preconnect, RSS alternate
- 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
2026-07-25 19:29:09 +02:00
itziarZG 97cbe25871 perf: switch inlineStylesheets to auto to avoid inlining Leaflet CSS 2026-07-25 19:28:55 +02:00
itziarZG 0c61483d10 feat(seo): add sitemap, RSS feed, and robots.txt 2026-07-25 19:28:13 +02:00
itziarZG b6c2e4f604 chore(deps): add @astrojs/sitemap @astrojs/rss 2026-07-25 19:27:55 +02:00
itziarZG b711069fe0 fix(map): import leaflet CSS statically so Vite emits the file
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.
2026-07-25 14:23:31 +02:00
itziarZG 8c68703f2c fix(astro): use www subdomain as canonical (apex lacks TLS) 2026-07-25 14:11:22 +02:00
itziarZG bf3b94be1a fix(astro): set site to production domain 2026-07-25 14:08:26 +02:00
itziarZG 69350257a6 build: pin Node >=22.12.0 (Astro 7.1.3 requirement) 2026-07-25 13:47:14 +02:00
itziarZG 11db4e7c86 fix(pnpm): add packages field to workspace 2026-07-25 13:20:18 +02:00
itziarZG 65425cfd2f chore: add .env.example template 2026-07-25 13:17:03 +02:00
itziarZG 566168e06f chore: initial Astro project scaffold 2026-07-25 11:19:24 +02:00