initial echo project website (rust + axum + askama)

This commit is contained in:
Jean Chevronnet 2026-08-30 18:21:49 +00:00
commit 899040a319
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
11 changed files with 1480 additions and 0 deletions

122
src/main.rs Normal file
View file

@ -0,0 +1,122 @@
//! The echoIRCd project website: a small Axum server that renders a handful of
//! compile-time Askama templates. Templates and static assets are baked into the
//! binary, so the deployed artifact is fully self-contained.
use askama::Template;
use axum::{
http::{header, StatusCode},
response::{Html, IntoResponse, Response},
routing::get,
Router,
};
use std::net::SocketAddr;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
active: &'static str,
}
#[derive(Template)]
#[template(path = "features.html")]
struct FeaturesTemplate {
active: &'static str,
}
#[derive(Template)]
#[template(path = "connect.html")]
struct ConnectTemplate {
active: &'static str,
}
/// Render a template to an HTML response, or a 500 if rendering fails.
fn page<T: Template>(t: T) -> Response {
match t.render() {
Ok(body) => Html(body).into_response(),
Err(e) => {
tracing::error!("template render error: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response()
}
}
}
async fn index() -> Response {
page(IndexTemplate { active: "home" })
}
async fn features() -> Response {
page(FeaturesTemplate { active: "features" })
}
async fn connect() -> Response {
page(ConnectTemplate { active: "connect" })
}
async fn style() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/css; charset=utf-8")],
include_str!("../static/style.css"),
)
}
async fn favicon() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "image/svg+xml")],
include_str!("../static/favicon.svg"),
)
}
async fn health() -> &'static str {
"ok"
}
async fn not_found() -> Response {
(
StatusCode::NOT_FOUND,
Html("<h1>404</h1><p>Not found. <a href=\"/\">Back home</a>.</p>"),
)
.into_response()
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let app = Router::new()
.route("/", get(index))
.route("/features", get(features))
.route("/connect", get(connect))
.route("/static/style.css", get(style))
.route("/favicon.svg", get(favicon))
.route("/health", get(health))
.fallback(not_found)
.layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http());
let addr: SocketAddr = std::env::var("ECHO_WEB_ADDR")
.unwrap_or_else(|_| "127.0.0.1:8099".into())
.parse()
.expect("ECHO_WEB_ADDR must be a valid socket address");
let listener = tokio::net::TcpListener::bind(addr)
.await
.unwrap_or_else(|e| panic!("bind {addr}: {e}"));
tracing::info!("echo website listening on http://{addr}");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown())
.await
.expect("server error");
}
/// Resolve on SIGINT (Ctrl-C) or SIGTERM (systemd stop) for a clean shutdown.
async fn shutdown() {
use tokio::signal::unix::{signal, SignalKind};
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}