fix(ssr): add Shell component and fix Leptos SSR configuration
Add Shell component wrapping the full HTML document (DOCTYPE, head, body)
required by leptos_meta. Add [package.metadata.leptos] to Cargo.toml and
switch get_configuration to Some("Cargo.toml"). Server now returns valid
HTML with title injection and WASM hydration scripts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,15 @@ console_error_panic_hook = { version = "0.1", optional = true }
|
|||||||
# Pont entre Rust/WASM et JavaScript : permet d'appeler du JS depuis Rust
|
# Pont entre Rust/WASM et JavaScript : permet d'appeler du JS depuis Rust
|
||||||
wasm-bindgen = { version = "0.2", optional = true }
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
# Configuration Leptos lue par get_configuration(Some("Cargo.toml"))
|
||||||
|
# Définit les chemins des fichiers compilés et l'adresse du serveur.
|
||||||
|
[package.metadata.leptos]
|
||||||
|
output-name = "rust-ipam" # Nom de base des fichiers .wasm et .js générés
|
||||||
|
site-root = "target/site" # Dossier racine des fichiers compilés par trunk
|
||||||
|
site-pkg-dir = "pkg" # Sous-dossier des assets WASM/JS dans site-root
|
||||||
|
site-addr = "127.0.0.1:3000" # Adresse d'écoute du serveur Axum
|
||||||
|
reload-port = 3001 # Port WebSocket pour le hot-reload en développement
|
||||||
|
|
||||||
# Profil de compilation WASM optimisé pour réduire la taille du fichier .wasm
|
# Profil de compilation WASM optimisé pour réduire la taille du fichier .wasm
|
||||||
# Un fichier WASM plus petit = page qui charge plus vite
|
# Un fichier WASM plus petit = page qui charge plus vite
|
||||||
[profile.wasm-release]
|
[profile.wasm-release]
|
||||||
|
|||||||
92
src/app.rs
92
src/app.rs
@@ -1,10 +1,8 @@
|
|||||||
// app.rs — Composant racine de l'application Leptos
|
// app.rs — Composants racine de l'application Leptos
|
||||||
//
|
//
|
||||||
// Ce composant est le point d'entrée de toute l'interface.
|
// Ce fichier contient deux composants :
|
||||||
// Il définit :
|
// - `Shell` : le document HTML complet (head + body) — SSR uniquement
|
||||||
// - Les métadonnées globales (title, CSS...)
|
// - `App` : le contenu de la page avec le routeur — partagé SSR + WASM
|
||||||
// - Le routeur : quelle page afficher selon l'URL
|
|
||||||
// - Les contextes globaux partagés (à ajouter plus tard : auth, thème...)
|
|
||||||
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
@@ -15,44 +13,88 @@ use leptos_router::{
|
|||||||
|
|
||||||
use crate::client::home::HomePage;
|
use crate::client::home::HomePage;
|
||||||
|
|
||||||
// `#[component]` est un attribut procédural Leptos.
|
// Shell — document HTML complet rendu par le serveur Axum
|
||||||
// Il transforme une fonction Rust normale en composant réutilisable et traçable.
|
|
||||||
//
|
//
|
||||||
// Règle de nommage : toujours PascalCase pour les composants Leptos.
|
// Ce composant n'existe qu'en mode SSR (`#[cfg(feature = "ssr")]`).
|
||||||
|
// Il fournit la structure HTML de base que leptos_meta ne peut pas créer seul :
|
||||||
|
// un <head> et un <body> valides. Sans ça, les composants <Title>, <Stylesheet>
|
||||||
|
// de leptos_meta n'ont nulle part où s'injecter.
|
||||||
//
|
//
|
||||||
// `-> impl IntoView` : la fonction retourne "quelque chose affichable".
|
// Flux de rendu SSR :
|
||||||
// On utilise `impl` (type opaque) car le type exact généré par `view!` est complexe.
|
// 1. Axum appelle Shell() pour chaque requête
|
||||||
|
// 2. Shell rend le <head> avec MetaTags (placeholder rempli par App)
|
||||||
|
// 3. Shell rend le <body> contenant App()
|
||||||
|
// 4. App() appelle provide_meta_context() et définit les métadonnées
|
||||||
|
// 5. Leptos collecte les métadonnées et les injecte dans MetaTags rétroactivement
|
||||||
|
// 6. HydrationScripts génère les <script> pour charger le bundle WASM
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
#[component]
|
||||||
|
pub fn Shell(
|
||||||
|
// LeptosOptions contient la config du projet (chemins, noms de fichiers, ports...)
|
||||||
|
// Utilisée par HydrationScripts pour construire les URLs du bundle WASM.
|
||||||
|
options: leptos::config::LeptosOptions,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
// MetaTags : placeholder où leptos_meta injecte les balises collectées
|
||||||
|
// depuis les composants <Title>, <Stylesheet>, <Meta>... définis dans App().
|
||||||
|
<MetaTags/>
|
||||||
|
// HydrationScripts : génère les balises <link> et <script> qui chargent
|
||||||
|
// le bundle WASM compilé par trunk et appellent la fonction hydrate() de lib.rs.
|
||||||
|
<HydrationScripts options=options.clone()/>
|
||||||
|
// AutoReload : hot-reload en développement (no-op en production).
|
||||||
|
// S'active uniquement si la variable d'env LEPTOS_WATCH est définie.
|
||||||
|
<AutoReload options/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
// App() s'exécute ici, fournit le contexte meta et rend le contenu de la page
|
||||||
|
<App/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App — composant racine partagé entre le serveur (SSR) et le navigateur (WASM)
|
||||||
|
//
|
||||||
|
// Ce composant est rendu :
|
||||||
|
// - côté serveur : dans le <body> du Shell, pour générer le HTML
|
||||||
|
// - côté navigateur : via hydrate() dans lib.rs, pour attacher la réactivité
|
||||||
|
//
|
||||||
|
// `-> impl IntoView` : retourne "quelque chose affichable". Le type exact est opaque
|
||||||
|
// car le compilateur Leptos génère un type complexe à partir de la macro `view!`.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
// Initialise le contexte de métadonnées Leptos.
|
// Initialise le système de métadonnées Leptos.
|
||||||
// Sans cet appel, les composants <Title>, <Meta>, <Stylesheet> plus bas ne fonctionnent pas.
|
// Sans cet appel, <Title>, <Stylesheet>, <Meta> dans les composants enfants
|
||||||
|
// n'auraient pas de contexte où stocker les métadonnées.
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
|
|
||||||
// La macro `view!` permet d'écrire du HTML dans du Rust.
|
|
||||||
// Leptos la transforme en code Rust pur à la compilation — pas de runtime template engine.
|
|
||||||
view! {
|
view! {
|
||||||
// Définit le titre de l'onglet navigateur (injecté dans le <head> HTML)
|
// Définit le titre de l'onglet navigateur
|
||||||
<Title text="Rust IPAM — Gestionnaire d'adresses IP"/>
|
<Title text="Rust IPAM — Gestionnaire d'adresses IP"/>
|
||||||
|
|
||||||
// Charge le CSS global. Le nom de fichier suit la convention Leptos :
|
// Charge le CSS global depuis /pkg/rust-ipam.css
|
||||||
// `{nom-du-crate}.css` dans le dossier `/pkg/` servi par Axum.
|
// Ce fichier est généré par trunk à partir de style.css (si ajouté plus tard)
|
||||||
<Stylesheet id="main" href="/pkg/rust-ipam.css"/>
|
<Stylesheet id="main" href="/pkg/rust-ipam.css"/>
|
||||||
|
|
||||||
// Le Router gère la navigation côté client sans rechargement de page.
|
// Le Router gère la navigation sans rechargement de page.
|
||||||
// Côté serveur (SSR), il détermine quel composant rendre selon l'URL demandée.
|
// Côté serveur, il détermine quel composant rendre selon l'URL.
|
||||||
<Router>
|
<Router>
|
||||||
<main>
|
<main>
|
||||||
// `<Routes>` est le conteneur pour toutes les définitions de routes.
|
// <Routes> est le conteneur pour toutes les définitions de routes.
|
||||||
// `fallback` est affiché si aucune route ne correspond à l'URL actuelle.
|
// `fallback` est affiché si aucune route ne correspond.
|
||||||
<Routes fallback=|| view! {
|
<Routes fallback=|| view! {
|
||||||
<div class="page-erreur">
|
<div class="page-erreur">
|
||||||
<h1>"404 — Page introuvable"</h1>
|
<h1>"404 — Page introuvable"</h1>
|
||||||
<a href="/">"← Retour à l'accueil"</a>
|
<a href="/">"← Retour à l'accueil"</a>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
// Chaque `<Route>` associe un chemin URL à un composant.
|
// path!(/) correspond à l'URL racine "/"
|
||||||
// `path!(/)` correspond exactement à l'URL racine "/".
|
// Ajouter de nouvelles pages ici :
|
||||||
// Ajouter de nouvelles pages ici, exemple :
|
|
||||||
// <Route path=path!("/reseaux") view=ReseauxPage/>
|
// <Route path=path!("/reseaux") view=ReseauxPage/>
|
||||||
<Route path=path!("/") view=HomePage/>
|
<Route path=path!("/") view=HomePage/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
64
src/main.rs
64
src/main.rs
@@ -12,75 +12,73 @@
|
|||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
// `#[tokio::main]` est une macro qui transforme notre `fn main()` synchrone
|
// `#[tokio::main]` transforme `fn main()` synchrone en une fonction asynchrone,
|
||||||
// en une fonction asynchrone, gérée par le runtime Tokio.
|
// gérée par le runtime Tokio. Sans ça, Rust ne sait pas exécuter du code `async`.
|
||||||
// Sans ça, Rust ne saurait pas comment exécuter du code `async`.
|
|
||||||
async fn main() {
|
async fn main() {
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::config::get_configuration;
|
use leptos::config::get_configuration;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use rust_ipam::{app::App, server::routes::not_found_handler};
|
use leptos::view;
|
||||||
|
use rust_ipam::{
|
||||||
|
app::{App, Shell},
|
||||||
|
server::routes::not_found_handler,
|
||||||
|
};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
// Initialise le système de logs structurés.
|
// Initialise les logs structurés.
|
||||||
// Les macros tracing::info!(), tracing::warn!(), tracing::error!()
|
// tracing::info!(), tracing::warn!(), etc. n'affichent rien sans cet initialisateur.
|
||||||
// n'affichent rien sans cet initialisateur.
|
// RUST_LOG=debug cargo run --features ssr → active les logs debug
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
// Lire le niveau de log depuis la variable d'environnement RUST_LOG,
|
|
||||||
// ou utiliser "info" par défaut si elle n'est pas définie.
|
|
||||||
// `unwrap_or_else` est une alternative idiomatique à `unwrap()` :
|
|
||||||
// elle fournit une valeur de repli au lieu de paniquer.
|
|
||||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
tracing::info!("Démarrage du serveur Rust IPAM...");
|
tracing::info!("Démarrage du serveur Rust IPAM...");
|
||||||
|
|
||||||
// Charge la configuration Leptos.
|
// `Some("Cargo.toml")` indique à Leptos de lire la section
|
||||||
// Leptos peut lire un fichier `Leptos.toml` ou utiliser des valeurs par défaut.
|
// [package.metadata.leptos] du Cargo.toml pour la configuration
|
||||||
// On utilise `.expect()` car le serveur ne peut pas fonctionner sans configuration.
|
// (noms de fichiers, chemins, adresse serveur...).
|
||||||
// La chaîne passée à expect() est affichée si la valeur est Err ou None.
|
let conf = get_configuration(Some("Cargo.toml"))
|
||||||
// Note : get_configuration() est synchrone en Leptos 0.7 — pas de .await ici.
|
.expect("Impossible de charger la configuration Leptos depuis Cargo.toml");
|
||||||
let conf = get_configuration(None)
|
|
||||||
.expect("Impossible de charger la configuration Leptos");
|
|
||||||
let leptos_options = conf.leptos_options;
|
let leptos_options = conf.leptos_options;
|
||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
|
|
||||||
// Analyse statiquement tous les composants `<Route>` dans `App`
|
// Analyse les composants `<Route>` dans `App` pour construire
|
||||||
// pour construire la liste des URLs que Leptos doit gérer.
|
// la liste des URLs que Leptos SSR doit gérer.
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
// Construit le routeur Axum avec le pattern builder.
|
// Construit le routeur Axum avec le pattern builder (chaînage de méthodes).
|
||||||
// Chaque méthode retourne un nouveau Router modifié — c'est du chaînage fonctionnel.
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
// Sert les fichiers statiques compilés par trunk (WASM, CSS, JS...).
|
// Sert les fichiers statiques compilés par trunk (WASM, JS...).
|
||||||
// `trunk build` les place dans `target/site/pkg/`.
|
// Trunk les place dans target/site/pkg/ (configuré dans [package.metadata.leptos]).
|
||||||
// Les navigateurs les demandent via des URLs comme `/pkg/rust-ipam.wasm`.
|
|
||||||
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
|
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
|
||||||
// Branche toutes les routes Leptos dans Axum.
|
// Branche les routes Leptos dans Axum.
|
||||||
// Pour chaque URL dans `routes`, Axum rend le composant App() en HTML.
|
// Pour chaque URL, Axum rend Shell() en HTML et le renvoie au navigateur.
|
||||||
.leptos_routes(&leptos_options, routes, App)
|
// Shell() contient App() qui fournit le contenu de la page.
|
||||||
// Handler de repli : toute URL non reconnue retourne une 404.
|
.leptos_routes(&leptos_options, routes, {
|
||||||
|
// On clone les options pour les capturer dans la closure.
|
||||||
|
// Le `move` transfère la propriété de `leptos_options` dans la closure.
|
||||||
|
let leptos_options = leptos_options.clone();
|
||||||
|
move || view! { <Shell options=leptos_options.clone()/> }
|
||||||
|
})
|
||||||
.fallback(not_found_handler)
|
.fallback(not_found_handler)
|
||||||
// Partage les options Leptos avec tous les handlers via le système d'état Axum.
|
// Partage les options Leptos avec tous les handlers via le système d'état Axum.
|
||||||
.with_state(leptos_options);
|
.with_state(leptos_options);
|
||||||
|
|
||||||
// Crée un listener TCP sur l'adresse configurée (par défaut 127.0.0.1:3000).
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr)
|
let listener = tokio::net::TcpListener::bind(&addr)
|
||||||
.await
|
.await
|
||||||
.expect(&format!("Impossible d'écouter sur l'adresse {}", addr));
|
.expect(&format!("Impossible d'écouter sur l'adresse {}", addr));
|
||||||
|
|
||||||
tracing::info!("Serveur disponible sur http://{}", addr);
|
tracing::info!("Serveur disponible sur http://{}", addr);
|
||||||
|
|
||||||
// Lance le serveur. Cette ligne bloque jusqu'à un Ctrl+C.
|
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
.await
|
.await
|
||||||
.expect("Erreur critique du serveur");
|
.expect("Erreur critique du serveur");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ce bloc vide est nécessaire pour que le compilateur trouve un `fn main()`
|
// Ce bloc vide est obligatoire pour que le compilateur trouve un `fn main()`
|
||||||
// quand on compile en mode WASM (où la feature "ssr" n'est pas activée).
|
// en mode WASM (où la feature "ssr" n'est pas activée).
|
||||||
// En WASM, le vrai point d'entrée est la fonction `hydrate()` dans lib.rs.
|
// En WASM, le vrai point d'entrée est la fonction `hydrate()` dans lib.rs.
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
fn main() {}
|
fn main() {}
|
||||||
|
|||||||
Reference in New Issue
Block a user