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:
2026-05-15 19:24:10 +02:00
parent efad573c3b
commit 1746d9d922
3 changed files with 107 additions and 58 deletions

View File

@@ -12,75 +12,73 @@
#[cfg(feature = "ssr")]
#[tokio::main]
// `#[tokio::main]` est une macro qui transforme notre `fn main()` synchrone
// en une fonction asynchrone, gérée par le runtime Tokio.
// Sans ça, Rust ne saurait pas comment exécuter du code `async`.
// `#[tokio::main]` transforme `fn main()` synchrone en une fonction asynchrone,
// gérée par le runtime Tokio. Sans ça, Rust ne sait pas exécuter du code `async`.
async fn main() {
use axum::Router;
use leptos::config::get_configuration;
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;
// Initialise le système de logs structurés.
// Les macros tracing::info!(), tracing::warn!(), tracing::error!()
// n'affichent rien sans cet initialisateur.
// Initialise les logs structurés.
// tracing::info!(), tracing::warn!(), etc. n'affichent rien sans cet initialisateur.
// RUST_LOG=debug cargo run --features ssr → active les logs debug
tracing_subscriber::fmt()
.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()),
)
.init();
tracing::info!("Démarrage du serveur Rust IPAM...");
// Charge la configuration Leptos.
// Leptos peut lire un fichier `Leptos.toml` ou utiliser des valeurs par défaut.
// On utilise `.expect()` car le serveur ne peut pas fonctionner sans configuration.
// La chaîne passée à expect() est affichée si la valeur est Err ou None.
// Note : get_configuration() est synchrone en Leptos 0.7 — pas de .await ici.
let conf = get_configuration(None)
.expect("Impossible de charger la configuration Leptos");
// `Some("Cargo.toml")` indique à Leptos de lire la section
// [package.metadata.leptos] du Cargo.toml pour la configuration
// (noms de fichiers, chemins, adresse serveur...).
let conf = get_configuration(Some("Cargo.toml"))
.expect("Impossible de charger la configuration Leptos depuis Cargo.toml");
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// Analyse statiquement tous les composants `<Route>` dans `App`
// pour construire la liste des URLs que Leptos doit gérer.
// Analyse les composants `<Route>` dans `App` pour construire
// la liste des URLs que Leptos SSR doit gérer.
let routes = generate_route_list(App);
// Construit le routeur Axum avec le pattern builder.
// Chaque méthode retourne un nouveau Router modifié — c'est du chaînage fonctionnel.
// Construit le routeur Axum avec le pattern builder (chaînage de méthodes).
let app = Router::new()
// Sert les fichiers statiques compilés par trunk (WASM, CSS, JS...).
// `trunk build` les place dans `target/site/pkg/`.
// Les navigateurs les demandent via des URLs comme `/pkg/rust-ipam.wasm`.
// Sert les fichiers statiques compilés par trunk (WASM, JS...).
// Trunk les place dans target/site/pkg/ (configuré dans [package.metadata.leptos]).
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
// Branche toutes les routes Leptos dans Axum.
// Pour chaque URL dans `routes`, Axum rend le composant App() en HTML.
.leptos_routes(&leptos_options, routes, App)
// Handler de repli : toute URL non reconnue retourne une 404.
// Branche les routes Leptos dans Axum.
// Pour chaque URL, Axum rend Shell() en HTML et le renvoie au navigateur.
// Shell() contient App() qui fournit le contenu de la page.
.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)
// Partage les options Leptos avec tous les handlers via le système d'état Axum.
.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)
.await
.expect(&format!("Impossible d'écouter sur l'adresse {}", addr));
tracing::info!("Serveur disponible sur http://{}", addr);
// Lance le serveur. Cette ligne bloque jusqu'à un Ctrl+C.
axum::serve(listener, app)
.await
.expect("Erreur critique du serveur");
}
// Ce bloc vide est nécessaire pour que le compilateur trouve un `fn main()`
// quand on compile en mode WASM (où la feature "ssr" n'est pas activée).
// Ce bloc vide est obligatoire pour que le compilateur trouve un `fn main()`
// 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.
#[cfg(not(feature = "ssr"))]
fn main() {}