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:
1 parent
5a04f159ca
commit
97b936fd3e
17 files changed
+2042
-829
No files matched your search
+154
-113
@@ -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 ".")
|
||||
Reference in new issue
Block a user