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.
This commit is contained in:
2026-07-26 21:24:54 +02:00
parent a09543512d
commit 9d8338cbef
9 changed files with 423 additions and 66 deletions
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<title>Ibiza Environmental Watch</title>
<rect width="1200" height="630" fill="#efeae0"/>
<rect x="54" y="54" width="1092" height="522" fill="#f8f5ee" stroke="#1f2a24" stroke-width="3"/>
<path d="M118 190c54-132 118-184 300-212-91 61-137 136-164 267-45-15-91-26-136-55z" fill="#2f6e51" transform="translate(0 96)"/>
<text x="118" y="354" fill="#1f2a24" font-family="Georgia, serif" font-size="62" font-weight="700">Ibiza</text>
<text x="310" y="354" fill="#1d4a37" font-family="Georgia, serif" font-size="62" font-style="italic" font-weight="700">Environmental</text>
<text x="118" y="425" fill="#1f2a24" font-family="Georgia, serif" font-size="62" font-weight="700">Watch</text>
<line x1="118" y1="478" x2="1082" y2="478" stroke="#d7cfbd" stroke-width="3"/>
<text x="118" y="530" fill="#4a5751" font-family="Arial, sans-serif" font-size="25">Vigilància ambiental d'Eivissa i Formentera</text>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

+11 -4
View File
@@ -5,6 +5,8 @@ interface Props {
aviso: Aviso;
}
const { aviso } = Astro.props;
const entryId = `aviso-${aviso.origen}-${aviso.id}`;
const titleId = `${entryId}-title`;
const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
day: 'numeric',
@@ -14,7 +16,9 @@ const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
---
<article
id={entryId}
class="entry"
aria-labelledby={titleId}
data-municipio={aviso.municipio ?? ''}
data-temas={aviso.temas.join('|')}
data-fuente={aviso.fuente}
@@ -27,7 +31,7 @@ const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
)
}
<h2 class="entry__title">
<h2 id={titleId} class="entry__title">
<a href={aviso.url} target="_blank" rel="noopener noreferrer">{aviso.titulo}<span class="visually-hidden"> (s'obre en una pestanya nova)</span></a>
</h2>
@@ -35,7 +39,7 @@ const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
{
aviso.temas.length > 0 && (
<ul class="entry__temas">
<ul class="entry__temas" aria-label="Temes de l'avís">
{aviso.temas.map((t) => (
<li>{t}</li>
))}
@@ -81,7 +85,10 @@ const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
}
.entry__title a {
color: var(--ink);
text-decoration: none;
text-decoration: underline;
text-decoration-color: var(--posidonia);
text-decoration-thickness: 0.08em;
text-underline-offset: 0.15em;
}
.entry__title a:hover {
color: var(--posidonia-deep);
@@ -107,7 +114,7 @@ const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--posidonia-deep);
background: color-mix(in srgb, var(--posidonia) 12%, transparent);
background: var(--posidonia-pale);
padding: 0.15rem 0.5rem;
border-radius: 999px;
}
+52 -23
View File
@@ -3,36 +3,41 @@ interface Props {
municipios: string[];
temas: string[];
fuentes: string[];
total: number;
}
const { municipios, temas, fuentes } = Astro.props;
const { municipios, temas, fuentes, total } = Astro.props;
const initialCount = `${total} avís${total === 1 ? '' : 'os'} ambiental${total === 1 ? '' : 's'}`;
---
<div class="filters" id="filters">
<div class="filters__group">
<span class="filters__label" id="filter-label-municipi">Municipi</span>
<div class="filters__pills" role="group" aria-labelledby="filter-label-municipi" data-filter-type="municipio">
<button type="button" class="pill is-active" aria-pressed="true" data-value="">Tots</button>
{municipios.map((m) => <button type="button" class="pill" aria-pressed="false" data-value={m}>{m}</button>)}
</div>
</div>
<div class="filters" id="filters" aria-describedby="filters-help">
<p class="filters__help" id="filters-help">Selecciona una opció de cada grup per limitar els avisos.</p>
<div class="filters__group">
<span class="filters__label" id="filter-label-temes">Tema</span>
<div class="filters__pills" role="group" aria-labelledby="filter-label-temes" data-filter-type="tema">
<button type="button" class="pill is-active" aria-pressed="true" data-value="">Tots</button>
{temas.map((t) => <button type="button" class="pill" aria-pressed="false" data-value={t}>{t}</button>)}
<fieldset class="filters__group">
<legend class="filters__label">Municipi</legend>
<div class="filters__pills" role="group" aria-label="Filtra per municipi" data-filter-type="municipio">
<button type="button" class="pill is-active" aria-pressed="true" aria-controls="feed-list" data-value="">Tots</button>
{municipios.map((m) => <button type="button" class="pill" aria-pressed="false" aria-controls="feed-list" data-value={m}>{m}</button>)}
</div>
</div>
</fieldset>
<div class="filters__group">
<span class="filters__label" id="filter-label-fonts">Font</span>
<div class="filters__pills" role="group" aria-labelledby="filter-label-fonts" data-filter-type="fuente">
<button type="button" class="pill is-active" aria-pressed="true" data-value="">Totes</button>
{fuentes.map((f) => <button type="button" class="pill" aria-pressed="false" data-value={f}>{f}</button>)}
<fieldset class="filters__group">
<legend class="filters__label">Tema</legend>
<div class="filters__pills" role="group" aria-label="Filtra per tema" data-filter-type="tema">
<button type="button" class="pill is-active" aria-pressed="true" aria-controls="feed-list" data-value="">Tots</button>
{temas.map((t) => <button type="button" class="pill" aria-pressed="false" aria-controls="feed-list" data-value={t}>{t}</button>)}
</div>
</div>
</fieldset>
<p class="filters__count" id="filters-count" aria-live="polite"></p>
<fieldset class="filters__group">
<legend class="filters__label">Font</legend>
<div class="filters__pills" role="group" aria-label="Filtra per font" data-filter-type="fuente">
<button type="button" class="pill is-active" aria-pressed="true" aria-controls="feed-list" data-value="">Totes</button>
{fuentes.map((f) => <button type="button" class="pill" aria-pressed="false" aria-controls="feed-list" data-value={f}>{f}</button>)}
</div>
</fieldset>
<p class="filters__count" id="filters-count" role="status" aria-live="polite" aria-atomic="true">{initialCount}</p>
<noscript><p class="filters__no-script">Activa JavaScript per utilitzar els filtres interactius. Tots els avisos es mostren a continuació.</p></noscript>
</div>
<style>
@@ -44,11 +49,23 @@ const { municipios, temas, fuentes } = Astro.props;
border-radius: var(--radius);
background: var(--paper-raised);
}
.filters__help {
margin: 0 0 var(--space-2);
color: var(--ink-soft);
font-size: 0.85rem;
}
.filters__group {
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
.filters__group + .filters__group {
margin-top: var(--space-1);
}
.filters__label {
display: block;
width: 100%;
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
@@ -62,6 +79,7 @@ const { municipios, temas, fuentes } = Astro.props;
gap: 0.4rem;
}
.pill {
min-height: 2.25rem;
font: inherit;
font-size: 0.8rem;
background: var(--paper);
@@ -76,11 +94,22 @@ const { municipios, temas, fuentes } = Astro.props;
border-color: var(--posidonia-deep);
color: var(--paper);
}
.pill:focus-visible {
outline-color: var(--ink);
}
.pill.is-active:focus-visible {
outline-color: var(--paper);
}
.filters__count {
margin: var(--space-1) 0 0;
font-size: 0.8rem;
color: var(--ink-soft);
}
.filters__no-script {
margin: var(--space-1) 0 0;
color: var(--ink-soft);
font-size: 0.8rem;
}
</style>
<script>
@@ -105,7 +134,7 @@ const { municipios, temas, fuentes } = Astro.props;
(!state.tema || temas.includes(state.tema)) &&
(!state.fuente || fuente === state.fuente);
entry.style.display = matches ? '' : 'none';
entry.hidden = !matches;
if (matches) visible += 1;
}
if (countEl) {
+56 -8
View File
@@ -5,21 +5,23 @@ import { municipioCoords } from '../lib/fixtures';
interface Props {
avisos: Aviso[];
descriptionId?: string;
}
/**
* Se hidrata solo en escritorio (ver index.astro, client:media). En móvil
* este componente ni se descarga: ni Leaflet, ni Preact, ni sus CSS.
*/
export default function MapView({ avisos }: Props) {
export default function MapView({ avisos, descriptionId }: Props) {
const mapEl = useRef<HTMLDivElement>(null);
useEffect(() => {
let map: import('leaflet').Map | undefined;
let cancelled = false;
(async () => {
const L = await import('leaflet');
if (!mapEl.current) return;
if (cancelled || !mapEl.current) return;
map = L.map(mapEl.current, {
center: [38.95, 1.43],
@@ -32,6 +34,10 @@ export default function MapView({ avisos }: Props) {
maxZoom: 18,
}).addTo(map);
const rootStyles = getComputedStyle(document.documentElement);
const markerColor = rootStyles.getPropertyValue('--posidonia-deep').trim() || '#1d4a37';
const markerFillColor = rootStyles.getPropertyValue('--posidonia').trim() || '#2f6e51';
// Agrupar avisos por municipio
const porMunicipio = new Map<string, Aviso[]>();
for (const a of avisos) {
@@ -48,32 +54,74 @@ export default function MapView({ avisos }: Props) {
const radius = 8 + Math.min(list.length, 10) * 2;
const marker = L.circleMarker(coords, {
radius,
color: '#1d4a37',
color: markerColor,
weight: 1,
fillColor: '#2f6e51',
fillColor: markerFillColor,
fillOpacity: 0.65,
}).addTo(map);
});
const items = list
.slice(0, 5)
.map(
(a) =>
`<li><a href="${a.url}" target="_blank" rel="noopener">${escapeHtml(a.titulo)}</a></li>`
`<li><a href="${escapeHtml(a.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(a.titulo)}<span class="visually-hidden"> (s'obre en una pestanya nova)</span></a></li>`
)
.join('');
const countLabel = `${list.length} avís${list.length === 1 ? '' : 'os'} ambiental${list.length === 1 ? '' : 's'}`;
const markerLabel = `${municipio}: ${countLabel}`;
let focusPopupLink = false;
let restoreMarkerFocus = false;
marker.bindPopup(
`<strong>${escapeHtml(municipio)}</strong> — ${list.length} avís${list.length === 1 ? '' : 'os'}<ul>${items}</ul>`
`<strong>${escapeHtml(municipio)}</strong> — ${countLabel}<ul>${items}</ul>`
);
marker.on('popupopen', () => {
if (!focusPopupLink) return;
focusPopupLink = false;
marker.getPopup()?.getElement()?.querySelector<HTMLAnchorElement>('a')?.focus();
});
marker.on('popupclose', () => {
if (!restoreMarkerFocus) return;
restoreMarkerFocus = false;
(marker.getElement() as HTMLElement | undefined)?.focus();
});
marker.on('add', () => {
const element = marker.getElement();
if (!element) return;
element.setAttribute('role', 'button');
element.setAttribute('tabindex', '0');
element.setAttribute('aria-label', markerLabel);
element.setAttribute('aria-keyshortcuts', 'Enter Space');
element.addEventListener('keydown', (event) => {
const key = (event as KeyboardEvent).key;
if (key !== 'Enter' && key !== ' ') return;
event.preventDefault();
focusPopupLink = true;
restoreMarkerFocus = true;
marker.openPopup();
});
});
marker.addTo(map);
}
})();
return () => {
cancelled = true;
map?.remove();
};
}, [avisos]);
return <div ref={mapEl} className="map-view" role="img" aria-label="Mapa d'avisos ambientals per municipi" />;
return (
<div
ref={mapEl}
className="map-view"
role="region"
aria-label="Mapa d'avisos ambientals per municipi"
aria-describedby={descriptionId}
tabIndex={0}
/>
);
}
function escapeHtml(s: string) {
+101 -11
View File
@@ -5,33 +5,88 @@ interface Props {
title: string;
description?: string;
ogImage?: string;
ogImageAlt?: string;
canonicalPath?: string;
ogType?: 'website' | 'article';
pageType?: 'WebPage' | 'CollectionPage' | 'AboutPage' | 'Article';
structuredData?: Record<string, unknown> | Record<string, unknown>[];
robots?: string;
}
const {
title,
description = "Vigilància ambiental d'Eivissa i Formentera: BOIB i premsa local, classificats amb IA local.",
ogImage,
ogImageAlt = 'Ibiza Environmental Watch',
canonicalPath,
ogType = 'website',
pageType = 'WebPage',
structuredData,
robots = 'index, follow, max-image-preview:large',
} = Astro.props;
const canonicalUrl = new URL(canonicalPath ?? Astro.url.pathname, Astro.site).href;
const siteUrl = (Astro.site ?? new URL(Astro.url.origin)).toString().replace(/\/$/, '');
const canonicalUrl = new URL(canonicalPath ?? Astro.url.pathname, siteUrl).href;
const socialImageUrl = new URL(ogImage ?? '/og-default.svg', siteUrl).href;
const logoUrl = new URL('/favicon.svg', siteUrl).href;
const isHome = Astro.url.pathname === '/';
const isHuella = Astro.url.pathname === '/huella';
const additionalSchemas = structuredData
? Array.isArray(structuredData)
? structuredData
: [structuredData]
: [];
const jsonLd = JSON.stringify({
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Organization',
'@id': `${siteUrl}#organization`,
name: 'Ibiza Environmental Watch',
url: siteUrl,
logo: {
'@type': 'ImageObject',
url: logoUrl,
},
},
{
'@type': 'WebSite',
'@id': `${siteUrl}#website`,
name: 'Ibiza Environmental Watch',
url: siteUrl,
description,
inLanguage: 'ca-ES',
publisher: { '@id': `${siteUrl}#organization` },
},
{
'@type': pageType,
'@id': `${canonicalUrl}#webpage`,
name: title,
url: canonicalUrl,
description,
inLanguage: 'ca-ES',
isPartOf: { '@id': `${siteUrl}#website` },
},
...additionalSchemas,
],
}).replace(/</g, '\\u003c');
---
<!doctype html>
<html lang="ca">
<html lang="ca" dir="ltr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="robots" content="index, follow" />
<meta name="author" content="Ibiza Environmental Watch" />
<meta name="robots" content={robots} />
<meta name="theme-color" content="#1d4a37" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="ca" href={canonicalUrl} />
<link rel="alternate" hreflang="x-default" href={canonicalUrl} />
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml" />
<link rel="preconnect" href="https://basemaps.cartocdn.com" crossorigin />
<link rel="alternate" type="application/rss+xml" title="Ibiza Environmental Watch" href="/rss.xml" />
@@ -41,24 +96,35 @@ const isHome = Astro.url.pathname === '/';
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content="Ibiza Environmental Watch" />
<meta property="og:locale" content="ca_ES" />
{ogImage && <meta property="og:image" content={new URL(ogImage, Astro.site).href} />}
<meta property="og:image" content={socialImageUrl} />
<meta property="og:image:alt" content={ogImageAlt} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{ogImage && <meta name="twitter:image" content={new URL(ogImage, Astro.site).href} />}
<meta name="twitter:image" content={socialImageUrl} />
<meta name="twitter:image:alt" content={ogImageAlt} />
<script is:inline type="application/ld+json" set:html={jsonLd}></script>
</head>
<body>
<a class="skip-link" href="#main-content">Saltar al contingut</a>
<header class="masthead">
<div class="masthead__inner">
<a class="masthead__title" href="/" aria-current={isHome ? 'page' : undefined}>
Ibiza <span>Environmental</span> Watch
</a>
<p class="masthead__subtitle">
BOIB i premsa d'Eivissa i Formentera, filtrats i resumits amb IA local &mdash; sense servidors de tercers.
</p>
<div class="masthead__brand">
<a class="masthead__title" href="/" aria-label="Ibiza Environmental Watch, inici">
Ibiza <span>Environmental</span> Watch
</a>
<p class="masthead__subtitle">
BOIB i premsa d'Eivissa i Formentera, filtrats i resumits amb IA local &mdash; sense servidors de tercers.
</p>
</div>
<nav class="masthead__nav" aria-label="Navegació principal">
<a href="/" aria-current={isHome ? 'page' : undefined}>Inici</a>
<a href="/huella" aria-current={isHuella ? 'page' : undefined}>Huella d'aquesta pàgina</a>
</nav>
</div>
</header>
@@ -107,6 +173,9 @@ const isHome = Astro.url.pathname === '/';
margin: 0 auto;
padding: var(--space-3) var(--space-2) var(--space-2);
}
.masthead__brand {
max-width: 42rem;
}
.masthead__title {
display: block;
font-family: var(--font-display);
@@ -129,6 +198,27 @@ const isHome = Astro.url.pathname === '/';
color: var(--ink-soft);
font-size: 0.95rem;
}
.masthead__nav {
display: flex;
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
margin-top: var(--space-2);
}
.masthead__nav a {
color: var(--ink);
font-size: 0.9rem;
font-weight: 600;
text-underline-offset: 0.2em;
}
.masthead__nav a[aria-current='page'] {
color: var(--posidonia-deep);
text-decoration-thickness: 0.15em;
}
main:focus {
outline: 3px solid var(--posidonia);
outline-offset: -3px;
}
.site-footer {
border-top: 1px solid var(--line);
+40
View File
@@ -0,0 +1,40 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout
title="Pàgina no trobada — Ibiza Environmental Watch"
description="La pàgina que cerques no existeix."
robots="noindex, follow"
>
<section class="not-found" aria-labelledby="not-found-title">
<p class="not-found__code">404</p>
<h1 id="not-found-title">Aquesta pàgina no existeix</h1>
<p>No hem trobat l'adreça sol·licitada. Torna a l'inici per consultar els avisos ambientals.</p>
<a class="not-found__link" href="/">Torna als avisos ambientals</a>
</section>
<style>
.not-found {
max-width: 42rem;
margin: 0 auto;
padding: var(--space-4) var(--space-2);
}
.not-found__code {
margin: 0;
color: var(--stamp);
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.08em;
}
h1 {
margin: var(--space-1) 0;
font-family: var(--font-display);
}
.not-found__link {
display: inline-block;
margin-top: var(--space-1);
font-weight: 600;
}
</style>
</BaseLayout>
+5 -1
View File
@@ -2,7 +2,11 @@
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Huella — Ibiza Environmental Watch">
<BaseLayout
title="Huella — Ibiza Environmental Watch"
description="Decisions de rendiment, allotjament i sostenibilitat d'Ibiza Environmental Watch."
pageType="AboutPage"
>
<section class="huella">
<h1>La huella d'aquesta pàgina</h1>
<p class="lead">
+137 -17
View File
@@ -10,57 +10,177 @@ const avisos = await getAvisosAmbientales();
const municipios = [...new Set(avisos.map((a) => a.municipio).filter(Boolean))].sort() as string[];
const temas = [...new Set(avisos.flatMap((a) => a.temas))].sort();
const fuentes = [...new Set(avisos.map((a) => a.fuente))].sort();
const avisosPorMunicipio = new Map<string, number>();
for (const aviso of avisos) {
const municipio = aviso.municipio ?? 'Municipi no especificat';
avisosPorMunicipio.set(municipio, (avisosPorMunicipio.get(municipio) ?? 0) + 1);
}
const resumenMunicipios = [...avisosPorMunicipio.entries()].sort(([a], [b]) => a.localeCompare(b, 'ca'));
const itemListSchema = {
'@type': 'ItemList',
name: 'Avisos ambientals dEivissa i Formentera',
numberOfItems: avisos.length,
itemListElement: avisos.map((aviso, index) => ({
'@type': 'ListItem',
position: index + 1,
item: {
'@type': 'Article',
headline: aviso.titulo,
description: aviso.resumen,
url: aviso.url,
datePublished: aviso.fecha,
author: {
'@type': 'Organization',
name: aviso.fuente,
},
},
})),
};
---
<BaseLayout title="Ibiza Environmental Watch — Avisos ambientals d'Eivissa i Formentera">
<section class="map-section">
<MapView avisos={avisos} client:media="(min-width: 900px)" />
<BaseLayout
title="Avisos ambientals d'Eivissa i Formentera | Ibiza Environmental Watch"
description="Avisos ambientals d'Eivissa i Formentera: informació del BOIB i de la premsa local, filtrada per municipi, tema i font."
pageType="CollectionPage"
structuredData={itemListSchema}
>
<div class="home-heading">
<h1 id="page-title" class="page-title">Avisos ambientals d'Eivissa i Formentera</h1>
<p class="page-intro">
Consulta informació del BOIB i de la premsa local sobre el medi ambient de les illes. Filtra els avisos per municipi, tema o font.
</p>
</div>
<section class="map-section" aria-labelledby="map-title">
<h2 id="map-title">Mapa d'avisos per municipi</h2>
<p id="map-description" class="map-description">
Mapa interactiu amb un marcador per municipi. Els marcadors es poden activar amb el teclat; també tens un resum textual a continuació.
</p>
<MapView avisos={avisos} descriptionId="map-description" client:media="(min-width: 900px)" />
</section>
<section class="feed">
<h1 class="feed__title">Avisos ambientals d'Eivissa i Formentera</h1>
<Filters municipios={municipios} temas={temas} fuentes={fuentes} />
<section class="map-alternative" aria-labelledby="map-alternative-title">
<h2 id="map-alternative-title">Resum d'avisos per municipi</h2>
<p>El resum textual ofereix la mateixa informació de distribució que el mapa.</p>
<ul>
{resumenMunicipios.map(([municipio, total]) => (
<li><span>{municipio}</span><span>{total} {total === 1 ? 'avís' : 'avisos'}</span></li>
))}
</ul>
</section>
<section class="feed" aria-labelledby="page-title">
<Filters municipios={municipios} temas={temas} fuentes={fuentes} total={avisos.length} />
{avisos.length === 0 && (
<p class="empty">No hi ha avisos ambientals classificats encara. Torna més tard.</p>
)}
<div class="feed__list">
<div class="feed__list" id="feed-list">
{avisos.map((aviso) => <EntryCard aviso={aviso} />)}
</div>
</section>
</BaseLayout>
<style>
.home-heading,
.feed,
.map-alternative {
max-width: 72rem;
margin: 0 auto;
padding-left: var(--space-2);
padding-right: var(--space-2);
}
.home-heading {
padding-top: var(--space-3);
}
.page-title,
.map-section h2,
.map-alternative h2 {
font-family: var(--font-display);
font-weight: 700;
color: var(--ink);
}
.page-title {
font-size: clamp(1.6rem, 3vw, 2rem);
margin: 0;
}
.page-intro {
max-width: 48rem;
margin: var(--space-1) 0 0;
color: var(--ink-soft);
}
.map-section {
display: none;
max-width: 72rem;
margin: var(--space-3) auto 0;
padding: 0 var(--space-2);
}
.map-section h2,
.map-alternative h2 {
font-size: 1.2rem;
margin: 0 0 var(--space-1);
}
.map-description {
margin: 0 0 var(--space-1);
color: var(--ink-soft);
font-size: 0.9rem;
}
@media (min-width: 900px) {
.map-section {
display: block;
}
}
.map-section :global(.map-view) {
height: 420px;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.feed {
max-width: 72rem;
margin: 0 auto;
padding: var(--space-3) var(--space-2) var(--space-4);
.map-alternative {
padding-top: var(--space-2);
}
.feed__title {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 var(--space-2);
color: var(--ink);
.map-alternative p {
margin: 0 0 var(--space-1);
color: var(--ink-soft);
font-size: 0.9rem;
}
.map-alternative ul {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 0.4rem 1.5rem;
list-style: none;
margin: 0;
padding: 0;
}
.map-alternative li {
display: flex;
justify-content: space-between;
gap: var(--space-1);
padding: 0.35rem 0;
border-bottom: 1px solid var(--line);
}
.map-alternative li span:last-child {
color: var(--ink-soft);
white-space: nowrap;
}
.feed {
padding-top: var(--space-3);
padding-bottom: var(--space-4);
}
.empty {
+10 -2
View File
@@ -17,7 +17,8 @@
--line: #d7cfbd;
--posidonia: #2f6e51;
--posidonia-deep: #1d4a37;
--stamp: #a8402f;
--posidonia-pale: #dce9df;
--stamp: #8a3025;
--font-display: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', Georgia, serif;
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@@ -55,11 +56,18 @@ a {
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible {
select:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
outline: 3px solid var(--posidonia);
outline-offset: 2px;
}
.leaflet-container .leaflet-interactive:focus-visible {
outline: 3px solid var(--posidonia-deep);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;