diff --git a/Dockerfile b/Dockerfile
index c84912d..ee5c44d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,6 @@
# ==========================================
# Stage 1: Build
# ==========================================
-# FROM rust:1.82-slim-bookworm AS builder
FROM rust:1.89-bookworm AS builder
WORKDIR /app
@@ -13,45 +12,51 @@ RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
-# Copy configuration files
-# COPY Cargo.toml ./
+# Copy Cargo metadata
COPY Cargo.toml Cargo.lock ./
-# Pre-build dependencies to cache them
+# Pre-build dependencies for layer caching
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
-# Copy source and templates
+# Copy application source
COPY src ./src
COPY templates ./templates
+COPY www ./www
-# Trigger rebuilding with actual source
+# Build application
RUN touch src/main.rs
RUN cargo build --release
# ==========================================
-# Stage 2: Runner
+# Stage 2: Runtime
# ==========================================
FROM debian:bookworm-slim
WORKDIR /app
-# Install runtime dependencies
+# Runtime dependencies
RUN apt-get update && apt-get install -y \
openssl \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
-# Copy binary from builder
+# Application binary
COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod
-# Create non-root user and data directory
+# Runtime assets
+COPY templates ./templates
+COPY www ./www
+
+# Create non-root user
RUN groupadd -g 1000 bzod && \
useradd -u 1000 -g bzod -m -s /bin/bash bzod
-RUN mkdir -p /app/data && chown -R bzod:bzod /app/data
+# Create writable data directory
+RUN mkdir -p /app/data && \
+ chown -R bzod:bzod /app
USER bzod
@@ -66,4 +71,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:$${PORT:-8654}/status || exit 1
ENTRYPOINT ["bzod"]
-CMD ["serve"]
+CMD ["serve"]
\ No newline at end of file
diff --git a/src/web/pages.rs b/src/web/pages.rs
index 3e4dd1d..df6f405 100644
--- a/src/web/pages.rs
+++ b/src/web/pages.rs
@@ -77,3 +77,47 @@ pub async fn resolve_page(
None => (StatusCode::NOT_FOUND, "Landing page not found").into_response(),
}
}
+
+// GET /
+// Serve static root landing page from www/index.html
+pub async fn root_landing() -> Response {
+ let mut target_path = std::path::PathBuf::from("www/index.html");
+
+ if !target_path.exists() {
+ // Search relative to executable
+ if let Ok(exe_path) = std::env::current_exe() {
+ if let Some(exe_dir) = exe_path.parent() {
+ let path1 = exe_dir.join("www/index.html");
+ if path1.exists() {
+ target_path = path1;
+ } else if let Some(parent1) = exe_dir.parent() {
+ let path2 = parent1.join("www/index.html");
+ if path2.exists() {
+ target_path = path2;
+ } else if let Some(parent2) = parent1.parent() {
+ let path3 = parent2.join("www/index.html");
+ if path3.exists() {
+ target_path = path3;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if !target_path.exists() {
+ // Search in CARGO_MANIFEST_DIR
+ if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
+ let path = std::path::PathBuf::from(manifest_dir).join("www/index.html");
+ if path.exists() {
+ target_path = path;
+ }
+ }
+ }
+
+ match std::fs::read_to_string(&target_path) {
+ Ok(content) => Html(content).into_response(),
+ Err(_) => (StatusCode::NOT_FOUND, "Not Found").into_response(),
+ }
+}
+
diff --git a/src/web/routes.rs b/src/web/routes.rs
index fbd6f10..9e92978 100644
--- a/src/web/routes.rs
+++ b/src/web/routes.rs
@@ -7,6 +7,8 @@ use axum::{
pub fn create_router(state: AppState) -> Router {
Router::new()
+ // --- Root Landing Page ---
+ .route("/", get(pages::root_landing))
// --- Public Redirection Routes ---
.route("/:code", get(redirect::resolve_redirect))
.route("/p/:code", get(pages::resolve_page))
diff --git a/tests/root_landing_tests.rs b/tests/root_landing_tests.rs
new file mode 100644
index 0000000..73ec074
--- /dev/null
+++ b/tests/root_landing_tests.rs
@@ -0,0 +1,43 @@
+use axum::http::StatusCode;
+use bzod::web::pages::root_landing;
+use std::fs;
+
+#[tokio::test]
+async fn test_root_landing_page() {
+ // --- Test case 1: Successful serving ---
+ // Read expected content
+ let expected_content = fs::read_to_string("www/index.html").expect("www/index.html must exist for test");
+
+ let response = root_landing().await;
+
+ assert_eq!(response.status(), StatusCode::OK);
+ assert!(
+ response.headers().get("content-type").unwrap().to_str().unwrap().contains("text/html")
+ );
+
+ // Convert response body to bytes using axum::body::to_bytes
+ let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
+ .await
+ .unwrap();
+ let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
+ assert_eq!(body_str, expected_content);
+
+ // --- Test case 2: File Not Found fallback ---
+ // Temporarily rename www/index.html to simulate file not found
+ let orig_path = "www/index.html";
+ let temp_path = "www/index.html.tmp_test_bak";
+ fs::rename(orig_path, temp_path).unwrap();
+
+ let response_res = tokio::spawn(async move {
+ root_landing().await
+ }).await;
+
+ // Restore index.html immediately in case of panic/error
+ let rename_res = fs::rename(temp_path, orig_path);
+
+ // Now verify the response status
+ let response = response_res.unwrap();
+ assert_eq!(response.status(), StatusCode::NOT_FOUND);
+
+ rename_res.unwrap();
+}
diff --git a/www/index.html b/www/index.html
index 3d8b149..d77dc64 100644
--- a/www/index.html
+++ b/www/index.html
@@ -1,182 +1,129 @@
+
-
-
+
+
- bzo.in - Simple & Private URL Shortener
+ BZOD — Private • Fast • Beautiful URL Shortener
+
+
-
-
-
-
BZOD • bzo.in
-
Admin
-
-
+
-
-
BETA
-
Short. Clean.
Privacy-First.
-
Lightning-fast URL shortener with built-in analytics, custom landing pages, and full self-hosting control.
+
+
+
+
+ ●
+ Self-hosted • Rust • Single Binary
+
+
+
+ Short links.
+ Your domain.
+
+
+
+ Beautiful, private, and powerful URL shortener with rich landing pages, QR codes, analytics, and full CLI control.
+
-
-
-
-
+
-
-
-
🔗 Compact Hex Codes
-
Short, memorable 4-6 character links that look professional.
-
-
-
📊 Real-time Analytics
-
Track clicks, countries, referrers with beautiful charts.
-
-
-
🎨 Custom Landing Pages
-
Beautiful branded pages before redirect (like this one!).
-
-
-
🔒 Self-Hosted & Private
-
Your data. Your server. No third-party tracking.
+
+ Powered by bzo.in
-
+
-