Sans SQL, juste du json pour le plaisir
url_for.Tu choisis un des 4 thèmes ci-dessous et tu réalises une mini-appli Flask avec 3 pages :
/?q=..../<prefix> (ex: /recettes, /films)id.?q= sur un ou deux champs (ex: titre, nom)./<prefix>/<int:id> (ex: /recettes/42)id n’existe pas : 404.Structure conseillée
app.py
data/<theme>.json
templates/
base.html
index.html
list.html
detail.html
static/style.css (optionnel)
Lecture JSON : json.load(open('data/<theme>.json', encoding='utf-8'))
Routage : @app.route('/<prefix>/<int:item_id>')
Recherche : request.args.get('q', '') (optionnel)
Jinja2 : extends "base.html", boucles {% for ... \%}, {{ url_for('detail', item_id=...) }}
404 propre : abort(404) si non trouvé.
Aucune écriture dans le JSON (lecture seule).
Pour réaliser ce projet, tu vas utiliser un admin template existant: https://adminlte.io/.
Le principe est "simple": il s'agit d'un template "prêt à l'emploi", avec des pages et des widgets réutilisables. Ton objectif: à partir de la documentation et des exemples/démos, tu vas "découper" le template (identifier les composants réutilisables) pour en récupérer le HTML nécessaire et l'intégrer dans ton site.
Pourquoi / avantages
base.html et des composants Jinja = moins de duplication.Comment s’y prendre (version rapide)
Installer les assets
base.html.static/ (ex. static/adminlte/…).Créer un layout global templates/base.html
<body class="layout-fixed">, navbar, sidebar, content-wrapper, footer.block title, block content, block scripts pour injecter le contenu des pages.Étendre le layout dans tes pages
index.html, list.html, detail.html → {% extends "base.html" %} + contenu dans block content.Composer avec des “partials”
navbar.html, sidebar.html, footer.html et include-les dans base.html pour réutiliser facilement.Intégrer les composants
{% for item in items %} dans une card ou table AdminLTE.Bonnes pratiques
base.html.app.css (dans static/) pour tes surcharges légères, pas de gros hacks.Mini-exemple de squelette base.html (CDN)
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{% block title %}Mon app{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 5 + AdminLTE 4 (CDN d’exemple) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@4/dist/css/adminlte.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
</head>
<body class="layout-fixed">
{% include "partials/navbar.html" %}
<div class="wrapper">
{% include "partials/sidebar.html" %}
<div class="content-wrapper p-3">
{% block content %}{% endblock %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/admin-lte@4/dist/js/adminlte.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
À retenir
url_for partout (pas d’URL en dur)base.html/recettesdata/recettes.json
[
{
"id": 1,
"nom": "Pâtes carbonara",
"categorie": "Plat",
"temps_min": 25,
"vegetarien": false,
"ingredients": ["Spaghetti", "Œufs", "Pancetta", "Parmesan", "Poivre"],
"etapes": [
"Cuire les pâtes al dente",
"Dorer la pancetta",
"Mélanger œufs + parmesan",
"Égoutter, hors du feu incorporer la sauce",
"Poivrer et servir"
],
"images": [
"/static/img/recettes/1_1.jpg",
"/static/img/recettes/1_2.jpg"
]
}
]
/filmsdata/films.json
[
{
"id": 101,
"titre": "Inception",
"annee": 2010,
"realisateur": "Christopher Nolan",
"duree_min": 148,
"genres": ["Science-fiction", "Thriller"],
"resume": "Un voleur infiltre les rêves pour extraire des secrets.",
"acteurs": [
{"nom": "Leonardo DiCaprio", "role": "Cobb"},
{"nom": "Joseph Gordon-Levitt", "role": "Arthur"},
{"nom": "Elliot Page", "role": "Ariadne"}
],
"images": [
"/static/img/films/101_poster.jpg",
"/static/img/films/101_scene1.jpg"
]
}
]
/eventsdata/events.json
[
{
"id": 2001,
"titre": "Quiz Musical — Années 90",
"date": "2025-09-20",
"ville": "Bruxelles",
"lieu": "Salle Delta",
"theme": "Musique",
"prix_eur": 8.5,
"description": "Un quiz musical convivial autour des tubes des années 90.",
"categories": [
{"nom": "Blind Test", "nb_questions": 10},
{"nom": "Paroles manquantes", "nb_questions": 8},
{"nom": "Covers & Remix", "nb_questions": 6}
],
"images": [
"/static/img/events/2001_affiche.jpg",
"/static/img/events/2001_scene.jpg"
]
}]
/cansatdata/cansat.json
[
{
"id": 501,
"equipe": "AstroPico",
"mission_secondaire": "Cartographier température & humidité",
"capteurs": ["DHT22", "BMP280", "GPS"],
"masse_g": 280,
"radio": "LoRa 433 MHz (démo), logs sur carte SD",
"notes": "Priorité à la robustesse mécanique.",
"membres": [
{"prenom": "Lina", "role": "Chef.fe d'équipe"},
{"prenom": "Yanis", "role": "Électronique/Capteurs"},
{"prenom": "Zoé", "role": "Traitement des données"}
],
"images": [
"/static/img/cansat/501_proto.jpg",
"/static/img/cansat/501_equipe.jpg"
]
}
]
Charger les données une fois au démarrage (module-level) ou à chaque requête si tu préfères la simplicité :
import json
with open('data/recettes.json', encoding='utf-8') as f:
RECETTES = json.load(f)
Retrouver un élément par id :
item = next((x for x in RECETTES if x["id"] == item_id), None)
puis abort(404) si item est None.
Lier vers le détail en Jinja :
<a href="{{ url_for('detail_recette', item_id=recette.id) }}">{{ recette.nom }}</a>
?sort= (ex: annee, nom), pagination simple ?page=2./.../<int:id>-<slug>.