heartwood every commit a ring
3.2 KB raw
use axum::{http::header, middleware as axum_middleware, Router};
use minijinja::Environment;
use std::path::PathBuf;
use std::sync::Arc;
use tower_http::services::ServeDir;
use tower_http::set_header::SetResponseHeaderLayer;

use crate::middleware::log_requests;
use crate::routes;
use crate::templates;

#[derive(Clone)]
pub struct AppState {
    pub env: Arc<Environment<'static>>,
    pub config: Arc<Config>,
}

#[derive(Debug, Clone)]
pub struct Config {
    pub root: PathBuf,
    pub repo_root: PathBuf,
    pub base_url: String,
    pub clone_base: String,
    pub site_title: String,
    pub site_tagline: String,
}

impl AppState {
    pub fn from_env() -> Self {
        let root: PathBuf = std::env::var("HEARTWOOD_ROOT")
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from("."));
        let repo_root: PathBuf = std::env::var("HEARTWOOD_REPO_ROOT")
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from("/srv/git"));
        let base_url = std::env::var("BASE_URL").unwrap_or_default();
        let clone_base = std::env::var("HEARTWOOD_CLONE_BASE")
            .unwrap_or_else(|_| "https://heartwood.bythewood.me".to_string());
        let site_title =
            std::env::var("HEARTWOOD_TITLE").unwrap_or_else(|_| "heartwood".to_string());
        let site_tagline = std::env::var("HEARTWOOD_TAGLINE")
            .unwrap_or_else(|_| "every commit a ring".to_string());

        let templates_dir = root.join("templates");
        let manifest_path = root.join("dist/.vite/manifest.json");
        let env = Arc::new(templates::build_env(&templates_dir, &manifest_path));

        let config = Arc::new(Config {
            root,
            repo_root,
            base_url,
            clone_base,
            site_title,
            site_tagline,
        });

        Self { env, config }
    }
}

pub fn router(state: AppState) -> Router {
    let dist_dir = state.config.root.join("dist");

    let static_cache = SetResponseHeaderLayer::if_not_present(
        header::CACHE_CONTROL,
        header::HeaderValue::from_static("public, max-age=31536000, immutable"),
    );

    Router::new()
        // Smart-HTTP clone endpoints live at /:name.git/... and need to be
        // matched before the catch-all repo routes; merge clone first.
        .merge(routes::clone::router())
        // seo routes (favicon, robots) before the repo catch-all so
        // /favicon.ico doesn't hit /:name.
        .merge(routes::seo::router())
        .merge(routes::index::router())
        .merge(routes::atom::router())
        .merge(routes::log::router())
        .merge(routes::commit::router())
        .merge(routes::tree::router())
        .merge(routes::blob::router())
        // repo landing page is the most general single-segment route, so
        // merge it last to avoid swallowing /static, /favicon.ico, etc.
        .merge(routes::repo::router())
        .nest_service(
            "/static",
            tower::ServiceBuilder::new()
                .layer(static_cache)
                .service(ServeDir::new(&dist_dir)),
        )
        .fallback(crate::middleware::not_found)
        .layer(axum_middleware::from_fn(log_requests))
        .with_state(state)
}