Skip to content

Commit cb3379f

Browse files
committed
feat: Introduce new Rust backend service and Nginx reverse proxy configuration.
1 parent 95dc3ce commit cb3379f

File tree

5 files changed

+121
-19
lines changed

5 files changed

+121
-19
lines changed

backend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ unicode-normalization = "0.1"
3636
sha2 = "0.10"
3737
hmac = "0.12"
3838
subtle = "2.5"
39+
reqwest = { version = "0.11", features = ["json"] }
3940

4041
[dependencies.home]
4142
version = "=0.5.9"
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use axum::{
2+
extract::State,
3+
response::{Html, IntoResponse},
4+
};
5+
use reqwest::Client;
6+
use std::env;
7+
use crate::{db, models::SiteContent};
8+
9+
// Default frontend URL (internal Docker network)
10+
const DEFAULT_FRONTEND_URL: &str = "http://frontend";
11+
12+
pub async fn serve_index(State(pool): State<db::DbPool>) -> impl IntoResponse {
13+
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| DEFAULT_FRONTEND_URL.to_string());
14+
let index_url = format!("{}/index.html", frontend_url);
15+
16+
// Fetch index.html from frontend container
17+
let client = Client::new();
18+
let html_content = match client.get(&index_url).send().await {
19+
Ok(resp) => match resp.text().await {
20+
Ok(text) => text,
21+
Err(e) => {
22+
tracing::error!("Failed to read index.html body: {}", e);
23+
return Html("<h1>Internal Server Error</h1><p>Failed to load application.</p>".to_string()).into_response();
24+
}
25+
},
26+
Err(e) => {
27+
tracing::error!("Failed to fetch index.html from {}: {}", index_url, e);
28+
return Html("<h1>Internal Server Error</h1><p>Failed to connect to frontend service.</p>".to_string()).into_response();
29+
}
30+
};
31+
32+
// Fetch site meta from DB
33+
let site_meta = match db::fetch_site_content_by_section(&pool, "site_meta").await {
34+
Ok(Some(record)) => {
35+
match serde_json::from_str::<serde_json::Value>(&record.content_json) {
36+
Ok(json) => json,
37+
Err(_) => serde_json::json!({}),
38+
}
39+
},
40+
_ => serde_json::json!({}),
41+
};
42+
43+
let title = site_meta.get("title")
44+
.and_then(|v| v.as_str())
45+
.unwrap_or("Linux Tutorial - Lerne Linux Schritt für Schritt");
46+
47+
let description = site_meta.get("description")
48+
.and_then(|v| v.as_str())
49+
.unwrap_or("Lerne Linux von Grund auf - Interaktiv, modern und praxisnah.");
50+
51+
// Inject meta tags using simple string replacement
52+
// We target the specific default tags to replace them
53+
let mut injected_html = html_content;
54+
55+
// Replace Title
56+
injected_html = injected_html.replace(
57+
"<title>Linux Tutorial - Lerne Linux Schritt für Schritt</title>",
58+
&format!("<title>{}</title>", title)
59+
);
60+
61+
// Replace Meta Description
62+
// Note: This regex-like replacement is brittle if the HTML formatting changes.
63+
// For now, we assume the exact string from index.html or use a more robust regex if needed.
64+
// Since we don't have regex crate here yet, we'll try to replace the known default description.
65+
// If it's dynamic, we might need a more robust approach, but for now let's try replacing the known default.
66+
let default_desc = "Lerne Linux von Grund auf - Interaktiv, modern und praxisnah. Umfassende Tutorials für Einsteiger und Fortgeschrittene.";
67+
injected_html = injected_html.replace(
68+
&format!("content=\"{}\"", default_desc),
69+
&format!("content=\"{}\"", description)
70+
);
71+
72+
// Also replace OG tags if possible.
73+
// A better approach for robust replacement without full HTML parsing:
74+
// We can replace the whole <head> block or specific known lines if we are sure about the structure.
75+
// Given the index.html structure, we can try to replace specific lines.
76+
77+
// Replace OG Title
78+
injected_html = injected_html.replace(
79+
"content=\"Linux Tutorial - Lerne Linux Schritt für Schritt\"",
80+
&format!("content=\"{}\"", title)
81+
);
82+
83+
// Replace OG Description (reusing the description replacement above might handle this if content matches)
84+
// The default OG description in index.html is shorter: "Lerne Linux von Grund auf - Interaktiv, modern und praxisnah."
85+
let default_og_desc = "Lerne Linux von Grund auf - Interaktiv, modern und praxisnah.";
86+
injected_html = injected_html.replace(
87+
&format!("content=\"{}\"", default_og_desc),
88+
&format!("content=\"{}\"", description)
89+
);
90+
91+
Html(injected_html).into_response()
92+
}

backend/src/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,4 @@ pub mod comments; // Comment system management
157157
pub mod site_content; // Dynamic site content sections
158158
pub mod site_pages; // Static page management
159159
pub mod site_posts; // Blog post management
160+
pub mod frontend_proxy; // Frontend proxy for server-side injection

backend/src/main.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,17 @@ async fn main() {
519519

520520
.route("/api/health", get(|| async { "OK" }))
521521

522-
.with_state(pool)
522+
.with_state(pool.clone())
523523
.layer(middleware::from_fn(security_headers))
524524
.layer(cors)
525525
.layer(RequestBodyLimitLayer::new(PUBLIC_BODY_LIMIT));
526526

527+
// Serve index.html with server-side injection for root and fallback
528+
// This must be added AFTER API routes to ensure they take precedence
529+
app = app.route("/", get(handlers::frontend_proxy::serve_index))
530+
.route("/*path", get(handlers::frontend_proxy::serve_index))
531+
.with_state(pool);
532+
527533
if !trust_proxy_ip_headers {
528534
app = app.layer(middleware::from_fn(strip_untrusted_forwarded_headers));
529535
}

nginx/nginx.conf

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -189,35 +189,37 @@ server {
189189
# FRONTEND ROUTING CONFIGURATION
190190
# ========================================================================
191191

192-
# Route all other requests to the frontend service
192+
# Route root and page requests to the backend for server-side injection
193+
# This allows the backend to inject dynamic meta tags into index.html
193194
location / {
194-
# Proxy to frontend upstream server
195-
proxy_pass http://frontend;
195+
# Proxy to backend upstream server
196+
proxy_pass http://backend;
196197

197198
# Use HTTP/1.1 for consistent behavior
198199
proxy_http_version 1.1;
199200

200-
# ====================================================================
201-
# PROXY HEADERS - FRONTEND REQUESTS
202-
# ====================================================================
203-
204-
# Standard proxy headers for frontend requests
201+
# Standard proxy headers
205202
proxy_set_header Host $host;
206203
proxy_set_header X-Real-IP $remote_addr;
207204
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
208205
proxy_set_header X-Forwarded-Proto $scheme;
206+
}
209207

210-
# ====================================================================
211-
# FRONTEND-SPECIFIC CONFIGURATION
212-
# ====================================================================
213-
214-
# Consider adding caching headers for static assets:
215-
# proxy_cache_valid 200 1h; # Cache successful responses
216-
# proxy_cache_key $scheme$proxy_host$request_uri;
208+
# Route static assets to the frontend service directly
209+
# This bypasses the backend for better performance on static files
210+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|json|xml|txt|map)$ {
211+
proxy_pass http://frontend;
212+
proxy_http_version 1.1;
213+
214+
# Standard proxy headers
215+
proxy_set_header Host $host;
216+
proxy_set_header X-Real-IP $remote_addr;
217+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
218+
proxy_set_header X-Forwarded-Proto $scheme;
217219

218-
# Compression support (if not handled by frontend):
219-
# gzip_proxied any;
220-
# gzip_vary on;
220+
# Caching headers for static assets
221+
expires 1y;
222+
add_header Cache-Control "public, immutable";
221223
}
222224

223225
# ========================================================================

0 commit comments

Comments
 (0)