Refonte : templates, balades, photos web, aperçus, carte globale

- Mise en page sortie du Python : Scripts/templates/ (Jinja2 + CSS/JS
  communs copiés dans assets/ de chaque voyage)
- Voyages rangés dans un sous-dossier (Balades/…) : rubriques sur
  l'accueil, 2 plus récents par rubrique, pages voyages.html et
  balades.html
- prepare_web.py : photos allégées (photos_web/) et images d'aperçu
  des vidéos (apercus/)
- Voyage d'une seule journée : la page du voyage est la carte du jour
- Carte globale de l'accueil (OpenStreetMap) : voyages du site + cartes
  Google My Maps récupérées (mymaps.txt, _mymaps/), moto par voyage
  (moto.txt)
- KML par voyage pour My Maps
- compress.sh : réencode une sortie illisible (encodage interrompu) ;
  repli automatique sur l'autre qualité dans la page
- Corrections : CSV vide (voyage sans photo), racine de genere_site.sh,
  fichiers ._ de macOS
- README réécrit, commentaires allégés

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
FabriceandClaude Opus 5.5 committed 2026-10-09 11:08:01 +00:00
1 parent 5a04f159ca
commit 97b936fd3e
17 files changed
+2042 -829

No files matched your search

+154 -113
View File
@@ -1,138 +1,179 @@
#!/usr/bin/env python3
"""Page d'accueil listant tous les voyages du site.
"""Page d'accueil du site et listes complètes par rubrique.
index.html une rubrique par section, avec les ACCUEIL_NB voyages les
plus récents (2 par défaut) et un lien « Tout voir »
voyages.html tous les voyages rangés à la racine du site
balades.html tous ceux du dossier Balades/ (une page par regroupement)
carte_globale.html les voyages (pas les balades) et les cartes My Maps
sur une même carte, affichée en grand sous les rubriques
de l'accueil (voir genere_carte_globale.py et mymaps.txt)
Usage : ./genere_index_general.py [racine_du_site]
Variable : ACCUEIL_NB=2
Un voyage est reconnu à la présence de son fichier mini.html (la carte
miniature produite par genere_carte.py).
Un voyage est un dossier qui contient mini.html (produit par genere_carte.py).
Les voyages rangés à la racine forment la rubrique « Voyages » ; ceux rangés
un niveau plus bas, dans un dossier de regroupement (Balades/Aisne-06-2026),
forment une rubrique du nom de ce dossier.
Un fichier moto.txt facultatif dans un voyage indique la moto (une ligne),
affichée dans la légende de la carte globale.
Les voyages sont triés du plus récent au plus ancien d'après les dates des
traces GPX (routes/AAAA-MM-JJ.gpx), sinon d'après l'année trouvée dans le nom.
"""
import os
import unicodedata
import re
import sys
import html
from datetime import date
from urllib.parse import quote
EXCLURE = {'html', 'Scripts', '__pycache__', '.git', 'node_modules', 'videos',
'photos', 'rushs', 'routes'}
OUTPUT_FILE = "index.html"
from jinja2 import Environment, FileSystemLoader
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from genere_carte_globale import SORTIE as CARTE_GLOBALE, generer_carte_globale # noqa: E402
DOSSIER_TEMPLATES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
EXCLURE = {'html', 'Scripts', 'RoadTripsGenerator', '__pycache__', 'node_modules',
'assets', 'videos', 'photos', 'photos_web', 'apercus', 'rushs', 'routes'}
TITRE_SITE = "Nos Road-Trips"
TITRE_SECTION_RACINE = "Voyages"
NB_RECENTS = int(os.environ.get("ACCUEIL_NB", 2))
MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet",
"août", "septembre", "octobre", "novembre", "décembre"]
def cle_de_tri(nom):
"""Trie du plus récent au plus ancien quand un millésime est détecté
dans le nom du dossier, alphabétiquement sinon."""
m = re.search(r'(19|20)\d{2}', nom)
annee = int(m.group(0)) if m else 0
return (-annee, nom.lower())
def sous_dossiers(dossier):
for d in sorted(os.listdir(dossier)):
chemin = os.path.join(dossier, d)
if d in EXCLURE or d.startswith(('.', '_')) or not os.path.isdir(chemin):
continue
yield d, chemin
def lien(trip, suffixe=""):
"""URL sûre : les espaces et les & des noms de dossier sont encodés."""
return html.escape(quote(trip) + suffixe)
def est_voyage(chemin):
return os.path.exists(os.path.join(chemin, 'mini.html'))
def dates_du_voyage(chemin):
routes = os.path.join(chemin, 'routes')
if not os.path.isdir(routes):
return []
dates = set()
for f in os.listdir(routes):
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', f)
if m and not f.startswith('.'):
try:
dates.add(date(*map(int, m.groups())))
except ValueError:
pass
return sorted(dates)
def periode(dates):
"""« juin 2026 · 1 jour », « juillet – août 2025 · 15 jours »…"""
if not dates:
return ""
d1, d2 = dates[0], dates[-1]
if (d1.year, d1.month) == (d2.year, d2.month):
texte = f"{MOIS[d1.month - 1]} {d1.year}"
elif d1.year == d2.year:
texte = f"{MOIS[d1.month - 1]} – {MOIS[d2.month - 1]} {d2.year}"
else:
texte = f"{MOIS[d1.month - 1]} {d1.year} – {MOIS[d2.month - 1]} {d2.year}"
n = len(dates)
return f"{texte} · {n} jour{'s' if n > 1 else ''}"
def lire_moto(chemin):
"""<voyage>/moto.txt (facultatif) : la ou les motos du voyage, sur une ligne."""
f = os.path.join(chemin, 'moto.txt')
if os.path.exists(f):
with open(f, encoding='utf-8') as fh:
return fh.readline().strip()
return ""
def fiche(racine, chemin):
nom = os.path.basename(chemin)
dates = dates_du_voyage(chemin)
if dates:
quand = dates[0]
else:
m = re.search(r'(19|20)\d{2}', nom)
quand = date(int(m.group(0)), 1, 1) if m else date.min
return {
'dossier': chemin,
'titre': nom.replace('-', ' ').replace('_', ' '),
'chemin': quote(os.path.relpath(chemin, racine).replace(os.sep, '/')),
'periode': periode(dates),
'moto': lire_moto(chemin),
'quand': quand,
}
def trier(voyages):
# Plus récent d'abord ; à date égale, ordre alphabétique.
voyages.sort(key=lambda v: v['titre'].lower())
voyages.sort(key=lambda v: v['quand'], reverse=True)
return voyages
def nom_de_page(titre):
"""« Balades » → « balades.html » (minuscules, sans accents ni espaces)."""
t = unicodedata.normalize('NFKD', titre).encode('ascii', 'ignore').decode().lower()
return re.sub(r'[^a-z0-9]+', '-', t).strip('-') + '.html'
def rubrique(titre, voyages):
n = len(voyages)
unite = titre.lower() if n > 1 else titre.lower().rstrip('s')
return {'titre': titre, 'voyages': trier(voyages), 'page': nom_de_page(titre), 'unite': unite}
def collecter_rubriques(racine):
principaux, rubriques = [], []
for nom, chemin in sous_dossiers(racine):
if est_voyage(chemin):
principaux.append(fiche(racine, chemin))
else:
voyages = [fiche(racine, c) for _, c in sous_dossiers(chemin) if est_voyage(c)]
if voyages:
rubriques.append(rubrique(nom.replace('-', ' ').replace('_', ' '), voyages))
if principaux:
rubriques.insert(0, rubrique(TITRE_SECTION_RACINE, principaux))
return rubriques
def generer_index_general(racine="."):
road_trips = []
for d in sorted(os.listdir(racine)):
if d in EXCLURE or d.startswith('.'):
continue
chemin = os.path.join(racine, d)
if os.path.isdir(chemin) and os.path.exists(os.path.join(chemin, 'mini.html')):
road_trips.append(d)
racine = os.path.abspath(racine)
rubriques = collecter_rubriques(racine)
total = sum(len(r['voyages']) for r in rubriques)
road_trips.sort(key=cle_de_tri)
env = Environment(loader=FileSystemLoader(DOSSIER_TEMPLATES), autoescape=True,
trim_blocks=True, lstrip_blocks=True)
cartes = []
for trip in road_trips:
# Le nom est échappé : un dossier « rando & vercors » produisait
# sinon du HTML invalide (& non échappé) et un lien cassé.
titre = html.escape(trip.replace('-', ' ').replace('_', ' '))
cartes.append(f""" <article class="card">
<iframe class="map-container" src="{lien(trip, '/mini.html')}"
loading="lazy" title="Carte de {titre}"></iframe>
<div class="card-content">
<h2>{titre}</h2>
<a href="{lien(trip, '/index.html')}" class="btn">Voir le voyage</a>
</div>
</article>""")
def ecrire(nom, contenu):
with open(os.path.join(racine, nom), "w", encoding="utf-8") as f:
f.write(contenu)
if road_trips:
corps = '<div class="grid">\n' + "\n".join(cartes) + '\n </div>'
sous_titre = f"{len(road_trips)} voyage" + ("s" if len(road_trips) > 1 else "")
else:
corps = ('<p class="vide">Aucun voyage pour l\'instant.<br>'
'Lance <code>genere_site.sh</code> pour en générer.</p>')
sous_titre = ""
ecrire("index.html", env.get_template("accueil.html").render(
titre_site=TITRE_SITE, rubriques=rubriques, nb_recents=NB_RECENTS,
carte_globale=CARTE_GLOBALE if any(r['titre'] == TITRE_SECTION_RACINE for r in rubriques) else None,
sous_titre=f"{total} voyage{'s' if total > 1 else ''}" if total else ""))
for r in rubriques:
ecrire(r['page'], env.get_template("liste.html").render(titre_site=TITRE_SITE, r=r))
# Pas d'espace avant <!DOCTYPE html> : la version précédente commençait par
# un saut de ligne et de l'indentation, ce qui bascule certains navigateurs
# en mode « quirks » et fausse la mise en page.
html_content = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nos Road-Trips</title>
<style>
:root {{ color-scheme: light dark; }}
* {{ box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f4f4f9; margin: 0; padding: 20px; color: #222;
}}
header {{ text-align: center; margin-bottom: 30px; }}
h1 {{ margin: 0 0 6px; font-size: clamp(1.6rem, 4vw, 2.4rem); }}
.sous-titre {{ color: #777; font-size: 0.9rem; }}
/* min() évite le débordement horizontal sur téléphone : l'ancienne règle
minmax(400px, 1fr) forçait 400 px de large sur un écran de 375 px. */
.grid {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(400px, 100%), 1fr));
gap: 30px; max-width: 1200px; margin: 0 auto;
}}
.card {{
background: #fff; border-radius: 15px; overflow: hidden;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
transition: transform .2s, box-shadow .2s;
}}
.card:hover {{ transform: translateY(-4px); box-shadow: 0 14px 28px rgba(0,0,0,0.15); }}
/* loading="lazy" : sans ça, ouvrir la page chargeait une carte Leaflet
complète par voyage, d'un coup. */
.map-container {{ width: 100%; height: 300px; border: none; pointer-events: none; display: block; }}
.card-content {{ padding: 20px; text-align: center; }}
h2 {{ text-transform: capitalize; margin: 0 0 14px; font-size: 1.25rem; }}
.btn {{
display: inline-block; padding: 10px 25px; background: #3498db; color: #fff;
text-decoration: none; border-radius: 25px; font-weight: bold;
}}
.btn:hover {{ background: #2980b9; }}
.vide {{ text-align: center; color: #777; margin-top: 60px; line-height: 1.7; }}
code {{ background: #e8e8ef; padding: 2px 6px; border-radius: 4px; }}
@media (prefers-color-scheme: dark) {{
body {{ background: #16161a; color: #e8e8ea; }}
.card {{ background: #232329; box-shadow: 0 10px 20px rgba(0,0,0,0.4); }}
.sous-titre, .vide {{ color: #9a9aa2; }}
code {{ background: #2e2e36; }}
}}
</style>
</head>
<body>
<header>
<h1>🌍 Nos Road-Trips</h1>
<div class="sous-titre">{sous_titre}</div>
</header>
<main>
{corps}
</main>
</body>
</html>
"""
# Carte globale : la rubrique Voyages seulement, sans les balades.
voyages = [r for r in rubriques if r['titre'] == TITRE_SECTION_RACINE]
if voyages:
generer_carte_globale(racine, voyages)
sortie = os.path.join(racine, OUTPUT_FILE)
with open(sortie, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"🌍 Index général : {len(road_trips)} voyage(s) → {sortie}")
detail = ", ".join(f"{r['titre']} : {len(r['voyages'])} → {r['page']}" for r in rubriques)
print(f"🌍 Accueil : {total} voyage(s) ({detail})")
if __name__ == "__main__":
racine = sys.argv[1] if len(sys.argv) > 1 else "."
generer_index_general(racine)
generer_index_general(sys.argv[1] if len(sys.argv) > 1 else ".")