info-canicule/src/layouts/Base.astro
Florian 72b3785499
All checks were successful
Deploy info-canicule / deploy (push) Successful in 1m30s
feat(design): refonte hi-fi (tokens, pills glyphes, dark mode, accueil)
Adopte le design system livré par Claude Design (Info Canicule.html) :
palette --paper/--brand/--ink + ramp vigilance, Public Sans + Manrope,
header sticky blurred avec toggle clair/sombre, pills vigilance avec
glyphes ●▲◆■ (a11y daltonisme), home restructurée (hero, stat tiles,
map + sidebar avec recherche département, liste filtrable, CTA conseils).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:41:57 +02:00

326 lines
14 KiB
Text

---
import '../styles/global.css';
interface Props {
title?: string;
description?: string;
canonical?: string;
ogImage?: string;
noindex?: boolean;
}
const SITE = 'https://info-canicule.nocleus.com';
const {
title = 'Info Canicule — Vigilance météo France en temps réel',
description = 'Suivi gratuit et sans publicité des alertes Vigilance Météo France (canicule, orages, tempêtes), avec carte interactive par département et conseils officiels.',
canonical,
ogImage = '/og-image.png',
noindex = false,
} = Astro.props;
const canonicalUrl = canonical ?? new URL(Astro.url.pathname, SITE).toString();
const fullOgImage = ogImage.startsWith('http') ? ogImage : `${SITE}${ogImage}`;
const umamiId = process.env.UMAMI_WEBSITE_ID;
const umamiSrc = process.env.UMAMI_SRC ?? 'https://analytics.nocleus.com/script.js';
const path = Astro.url.pathname;
const isActive = (p: string) => (p === '/' ? path === '/' : path.startsWith(p));
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebSite',
'@id': `${SITE}/#website`,
name: 'Info Canicule',
url: SITE,
description,
inLanguage: 'fr-FR',
publisher: { '@type': 'Person', name: 'Florian Bouchet' },
potentialAction: {
'@type': 'SearchAction',
target: `${SITE}/departement/{code}`,
'query-input': 'required name=code',
},
},
{
'@type': 'Service',
'@id': `${SITE}/#service`,
name: 'Info Canicule',
serviceType: "Service d'information météorologique grand public",
areaServed: { '@type': 'Country', name: 'France' },
audience: { '@type': 'PeopleAudience', audienceType: 'Grand public, personnes fragiles' },
provider: {
'@type': 'Person',
name: 'Florian Bouchet',
url: `${SITE}/a-propos`,
},
isBasedOn: {
'@type': 'Dataset',
name: 'Vigilance Météo France',
url: 'https://vigilance.meteofrance.fr/',
creator: { '@type': 'Organization', name: 'Météo-France' },
license: 'https://www.etalab.gouv.fr/licence-ouverte-open-licence/',
},
isAccessibleForFree: true,
},
],
};
---
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content={description} />
{noindex ? <meta name="robots" content="noindex, nofollow" /> : <meta name="robots" content="index, follow, max-image-preview:large" />}
<link rel="canonical" href={canonicalUrl} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="Info Canicule" href={`${SITE}/sitemap-index.xml`} />
<title>{title}</title>
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={fullOgImage} />
<meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content="Info Canicule" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullOgImage} />
<meta name="theme-color" content="#fdfaf2" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#14110d" media="(prefers-color-scheme: dark)" />
<meta name="format-detection" content="telephone=yes" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&family=Manrope:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
{/* Avoid flash of light theme on dark-preferring devices */}
<script is:inline>
(function () {
try {
var saved = localStorage.getItem('ic-theme');
var mql = window.matchMedia('(prefers-color-scheme: dark)');
var theme = saved || (mql.matches ? 'dark' : 'light');
if (theme === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
} catch (_) {}
})();
</script>
{umamiId && (
<script defer src={umamiSrc} data-website-id={umamiId} data-do-not-track="true"></script>
)}
</head>
<body class="flex flex-col" style="min-height: 100dvh;">
<a href="#main" class="skip-link">Aller au contenu principal</a>
<header class="site-header">
<div class="container-tight flex items-center justify-between gap-3 py-3.5">
<a href="/" class="inline-flex items-center gap-2.5 font-display text-[1.1rem] font-extrabold no-underline" style="color: var(--ink);">
<svg width="28" height="28" viewBox="0 0 32 32" aria-hidden="true">
<circle cx="16" cy="16" r="14" fill="var(--sun)" />
<circle cx="16" cy="16" r="11" fill="var(--brand)" />
<path d="M16 7 C12 11 12 16 16 21 C20 16 20 11 16 7 Z" fill="#fff" />
</svg>
<span>Info Canicule</span>
</a>
<nav id="nav-main" class="nav-links flex items-center gap-1" aria-label="Navigation principale">
<a href="/" class:list={['nav-link', isActive('/') && 'is-active']}>Carte</a>
<a href="/conseils" class:list={['nav-link', isActive('/conseils') && 'is-active']}>Conseils</a>
<a href="/a-propos" class:list={['nav-link', isActive('/a-propos') && 'is-active']}>À propos</a>
<a href="/soutenir" class:list={['nav-link', isActive('/soutenir') && 'is-active']} style="color: var(--brand-deep);">☕ Soutenir</a>
</nav>
<div class="flex items-center gap-2">
<button
id="theme-toggle"
type="button"
class="icon-btn"
aria-label="Basculer entre mode clair et mode sombre"
title="Basculer mode clair / sombre"
>
<svg id="theme-icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
</svg>
<svg id="theme-icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none;">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
</button>
<button
id="nav-toggle"
type="button"
class="icon-btn"
data-mobile-only
aria-label="Ouvrir le menu"
aria-expanded="false"
aria-controls="nav-main"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
</div>
</div>
</header>
<main id="main" class="flex-1">
<slot />
</main>
<footer class="site-footer">
<div class="container-tight py-8">
<div class="grid gap-8" style="grid-template-columns: 1.4fr 1fr 1fr 1fr;">
<div>
<a href="/" class="inline-flex items-center gap-2.5 font-display text-[1.05rem] font-extrabold no-underline" style="color: var(--ink);">
<svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true">
<circle cx="16" cy="16" r="14" fill="var(--sun)" />
<circle cx="16" cy="16" r="11" fill="var(--brand)" />
<path d="M16 7 C12 11 12 16 16 21 C20 16 20 11 16 7 Z" fill="#fff" />
</svg>
Info Canicule
</a>
<p class="mt-2 text-sm" style="color: var(--ink-soft); max-width: 36ch;">
Service d'information publique gratuit, sans publicité, non lucratif.
</p>
</div>
<div>
<h4>Le site</h4>
<ul class="flex flex-col gap-2 text-sm">
<li><a href="/a-propos" style="color: var(--ink-2);">À propos</a></li>
<li><a href="/conseils" style="color: var(--ink-2);">Conseils par phénomène</a></li>
<li><a href="/soutenir" style="color: var(--ink-2);">☕ Soutenir le projet</a></li>
</ul>
</div>
<div>
<h4>Mentions</h4>
<ul class="flex flex-col gap-2 text-sm">
<li><a href="/mentions-legales" style="color: var(--ink-2);">Mentions légales</a></li>
<li><a href="/dependances" style="color: var(--ink-2);">Dépendances</a></li>
<li><a href={`${SITE}/sitemap-index.xml`} style="color: var(--ink-2);">Plan du site</a></li>
</ul>
</div>
<div>
<h4>Données</h4>
<ul class="flex flex-col gap-2 text-sm">
<li>
<a href="https://meteo.data.gouv.fr/" rel="noopener" style="color: var(--ink-2);">Météo France (open data)</a>
</li>
<li>
<a href="https://www.etalab.gouv.fr/licence-ouverte-open-licence/" rel="noopener" style="color: var(--ink-2);">Licence Ouverte 2.0</a>
</li>
<li>
En urgence : <a href="tel:112" style="color: var(--brand-deep); font-weight: 600;">112</a>
</li>
</ul>
</div>
</div>
<div class="mt-7 flex flex-wrap items-center justify-between gap-3 border-t pt-5 text-xs" style="border-color: var(--line); color: var(--ink-soft);">
<span>Édité à titre personnel, sans but lucratif.</span>
<span>
Données officielles · <a href="https://vigilance.meteofrance.fr/" rel="noopener" style="color: var(--brand-deep);">vigilance.meteofrance.fr</a>
</span>
</div>
</div>
</footer>
<style>
/* Header nav links */
.nav-link {
color: var(--ink-2);
padding: 8px 14px;
border-radius: var(--r-md);
font-weight: 500;
font-size: 0.95rem;
text-decoration: none;
border-bottom: none;
}
.nav-link:hover { background: var(--paper-warm); color: var(--ink); border-bottom: none; }
.nav-link.is-active { color: var(--ink); background: var(--paper-warm); }
@media (max-width: 720px) {
.nav-links {
display: none !important;
}
.nav-links.open {
display: flex !important;
flex-direction: column;
align-items: stretch;
position: fixed;
inset: 60px 0 auto 0;
background: var(--paper-2);
padding: 12px;
border-bottom: 1px solid var(--line);
box-shadow: var(--sh-3);
gap: 4px;
z-index: 49;
}
.nav-links.open .nav-link { width: 100%; padding: 14px 18px; }
}
[data-mobile-only] { display: none !important; }
@media (max-width: 720px) {
[data-mobile-only] { display: inline-flex !important; }
}
@media (max-width: 900px) {
.site-footer > div > div:first-child { grid-template-columns: 1fr 1fr !important; }
}
@media (max-width: 900px) {
.site-footer .grid { grid-template-columns: 1fr 1fr !important; }
}
@media (max-width: 540px) {
.site-footer .grid { grid-template-columns: 1fr !important; }
}
</style>
<script is:inline>
(function () {
var root = document.documentElement;
var toggle = document.getElementById('theme-toggle');
var iconSun = document.getElementById('theme-icon-sun');
var iconMoon = document.getElementById('theme-icon-moon');
function syncIcons() {
var dark = root.getAttribute('data-theme') === 'dark';
if (iconSun) iconSun.style.display = dark ? 'none' : '';
if (iconMoon) iconMoon.style.display = dark ? '' : 'none';
}
syncIcons();
if (toggle) {
toggle.addEventListener('click', function () {
var dark = root.getAttribute('data-theme') === 'dark';
if (dark) { root.removeAttribute('data-theme'); }
else { root.setAttribute('data-theme', 'dark'); }
try { localStorage.setItem('ic-theme', dark ? 'light' : 'dark'); } catch (_) {}
syncIcons();
});
}
var navToggle = document.getElementById('nav-toggle');
var nav = document.getElementById('nav-main');
if (navToggle && nav) {
navToggle.addEventListener('click', function () {
var open = nav.classList.toggle('open');
navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
});
nav.addEventListener('click', function (e) {
if (e.target && e.target.tagName === 'A') {
nav.classList.remove('open');
navToggle.setAttribute('aria-expanded', 'false');
}
});
}
})();
</script>
</body>
</html>