From b30a6a102bd245074120c82b93dd48b6eafe8b2b Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 4 May 2026 22:38:58 +0200 Subject: [PATCH] feat: per-page titles, OG/SEO tags, HOST/PORT env vars, BASE_URL in config --- .../template-askama/templates/base.html | 10 +++++++++- crates/application/src/config.rs | 5 ++++- crates/application/src/ports.rs | 2 ++ crates/presentation/src/event_handlers.rs | 2 +- crates/presentation/src/extractors.rs | 6 +++--- crates/presentation/src/handlers.rs | 19 +++++++++++++++++-- crates/presentation/src/main.rs | 7 +++++-- crates/presentation/tests/api_test.rs | 2 +- 8 files changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/adapters/template-askama/templates/base.html b/crates/adapters/template-askama/templates/base.html index 8df3e0d..d048c93 100644 --- a/crates/adapters/template-askama/templates/base.html +++ b/crates/adapters/template-askama/templates/base.html @@ -3,7 +3,15 @@ - Movies Diary + {{ ctx.page_title }} + + + + + + + + diff --git a/crates/application/src/config.rs b/crates/application/src/config.rs index 84c3cce..c64eeff 100644 --- a/crates/application/src/config.rs +++ b/crates/application/src/config.rs @@ -1,6 +1,7 @@ #[derive(Clone)] pub struct AppConfig { pub allow_registration: bool, + pub base_url: String, } impl AppConfig { @@ -8,6 +9,8 @@ impl AppConfig { let allow_registration = std::env::var("ALLOW_REGISTRATION") .map(|v| v == "true" || v == "1") .unwrap_or(false); - Self { allow_registration } + let base_url = std::env::var("BASE_URL") + .unwrap_or_else(|_| "http://localhost:3000".to_string()); + Self { allow_registration, base_url } } } diff --git a/crates/application/src/ports.rs b/crates/application/src/ports.rs index adcb3fc..5521cad 100644 --- a/crates/application/src/ports.rs +++ b/crates/application/src/ports.rs @@ -7,6 +7,8 @@ pub struct HtmlPageContext { pub user_id: Option, pub register_enabled: bool, pub rss_url: String, + pub page_title: String, + pub canonical_url: String, } impl HtmlPageContext { diff --git a/crates/presentation/src/event_handlers.rs b/crates/presentation/src/event_handlers.rs index 5179081..ded1938 100644 --- a/crates/presentation/src/event_handlers.rs +++ b/crates/presentation/src/event_handlers.rs @@ -160,7 +160,7 @@ mod tests { auth_service: Arc::new(PanicAuth), password_hasher: Arc::new(PanicHasher), user_repository: Arc::new(PanicUserRepo), - config: AppConfig { allow_registration: false }, + config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() }, } } diff --git a/crates/presentation/src/extractors.rs b/crates/presentation/src/extractors.rs index e1e35ed..4deb4f4 100644 --- a/crates/presentation/src/extractors.rs +++ b/crates/presentation/src/extractors.rs @@ -175,7 +175,7 @@ mod tests { auth_service: Arc::new(PanicAuth), password_hasher: Arc::new(PanicHasher), user_repository: Arc::new(PanicUserRepo), - config: application::config::AppConfig { allow_registration: false }, + config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() }, }, html_renderer: Arc::new(PanicRenderer), rss_renderer: Arc::new(PanicRssRenderer), @@ -282,7 +282,7 @@ mod tests { auth_service: Arc::new(PanicAuth2), password_hasher: Arc::new(PanicHasher2), user_repository: Arc::new(PanicUserRepo2), - config: application::config::AppConfig { allow_registration: false }, + config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() }, }, html_renderer: Arc::new(PanicRenderer2), rss_renderer: Arc::new(PanicRssRenderer2), @@ -341,7 +341,7 @@ mod tests { auth_service: Arc::new(RejectingAuth), password_hasher: Arc::new(PanicHasher3), user_repository: Arc::new(PanicUserRepo3), - config: application::config::AppConfig { allow_registration: false }, + config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() }, }, html_renderer: Arc::new(PanicRenderer3), rss_renderer: Arc::new(PanicRssRenderer3), diff --git a/crates/presentation/src/handlers.rs b/crates/presentation/src/handlers.rs index 47b04f1..30462d7 100644 --- a/crates/presentation/src/handlers.rs +++ b/crates/presentation/src/handlers.rs @@ -43,6 +43,8 @@ pub mod html { user_id: uuid, register_enabled: state.app_ctx.config.allow_registration, rss_url: "/feed.rss".to_string(), + page_title: "Movies Diary".to_string(), + canonical_url: state.app_ctx.config.base_url.clone(), } } @@ -74,6 +76,8 @@ pub mod html { user_id: None, register_enabled: state.app_ctx.config.allow_registration, rss_url: "/feed.rss".to_string(), + page_title: "Login — Movies Diary".to_string(), + canonical_url: format!("{}/login", state.app_ctx.config.base_url), }; let html = state .html_renderer @@ -125,6 +129,8 @@ pub mod html { user_id: None, register_enabled: true, rss_url: "/feed.rss".to_string(), + page_title: "Register — Movies Diary".to_string(), + canonical_url: format!("{}/register", state.app_ctx.config.base_url), }; let html = state .html_renderer @@ -175,7 +181,9 @@ pub mod html { State(state): State, Query(params): Query, ) -> impl IntoResponse { - let ctx = build_page_context(&state, Some(user_id)).await; + let mut ctx = build_page_context(&state, Some(user_id)).await; + ctx.page_title = "Log a Review — Movies Diary".to_string(); + ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url); let html = state .html_renderer .render_new_review_page(NewReviewPageData { @@ -262,7 +270,9 @@ pub mod html { OptionalCookieUser(user_id): OptionalCookieUser, State(state): State, ) -> impl IntoResponse { - let ctx = build_page_context(&state, user_id).await; + let mut ctx = build_page_context(&state, user_id).await; + ctx.page_title = "Members — Movies Diary".to_string(); + ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url); match application::use_cases::get_users::execute(&state.app_ctx, application::queries::GetUsersQuery).await { Ok(users) => { let data = application::ports::UsersPageData { ctx, users }; @@ -293,6 +303,11 @@ pub mod html { Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; + let display_name = profile_user.email().value() + .split('@').next().unwrap_or("User"); + ctx.page_title = format!("{}'s Diary — Movies Diary", display_name); + ctx.canonical_url = format!("{}/users/{}", state.app_ctx.config.base_url, profile_user_uuid); + let query = application::queries::GetUserProfileQuery { user_id: profile_user_uuid, view: view.clone(), diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index db1ed5c..a5afadc 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -32,8 +32,11 @@ async fn main() -> anyhow::Result<()> { let app = routes::build_router(state); - let listener = TcpListener::bind("0.0.0.0:3000").await?; - tracing::info!("Listening on 0.0.0.0:3000"); + let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); + let addr = format!("{}:{}", host, port); + let listener = TcpListener::bind(&addr).await?; + tracing::info!("Listening on {}", addr); axum::serve(listener, app).await?; Ok(()) diff --git a/crates/presentation/tests/api_test.rs b/crates/presentation/tests/api_test.rs index b13b229..5670d31 100644 --- a/crates/presentation/tests/api_test.rs +++ b/crates/presentation/tests/api_test.rs @@ -105,7 +105,7 @@ async fn test_app() -> Router { auth_service: Arc::new(PanicAuth), password_hasher: Arc::new(PanicHasher), user_repository: Arc::new(NobodyUserRepo), - config: AppConfig { allow_registration: false }, + config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() }, }, html_renderer: Arc::new(AskamaHtmlRenderer::new()), rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),