replaced askama with tera, reworked the whole website and added nixos module

This commit is contained in:
2026-03-26 13:24:13 +01:00
parent 0670589b5e
commit 81686d6fb8
12 changed files with 888 additions and 621 deletions

905
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "mycoolwebsite" name = "quick-rust-website"
version = "0.1.1" version = "0.1.1"
edition = "2024" edition = "2024"
@@ -9,8 +9,10 @@ tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.43" tracing = "0.1.43"
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.22"
tower-http = { version = "0.6.6", features = ["cors", "limit"] } tower-http = { version = "0.6.6", features = ["cors", "limit"] }
tower_governor = "0.8.0"
anyhow = "1.0.100" anyhow = "1.0.100"
chrono = { version = "0.4.42", features = ["serde"] }
askama = "0.14.0"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
tera = "1.20"
lazy_static = "1.5"
tower_governor = "0.8.0"
serde = { version = "1.0.228", features = ["derive"] }
chrono = { version = "0.4.44", features = ["serde"] }

71
flake.lock generated
View File

@@ -1,23 +1,5 @@
{ {
"nodes": { "nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1764950072, "lastModified": 1764950072,
@@ -34,60 +16,9 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1765161692,
"narHash": "sha256-XdY9AFzmgRPYIhP4N+WiCHMNxPoifP5/Ld+orMYBD8c=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "7ed7e8c74be95906275805db68201e74e9904f07",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
} }
} }
}, },

123
flake.nix
View File

@@ -1,57 +1,114 @@
{ {
description = "My cool website"; description = "Quick rust website template";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
}; };
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }: outputs =
flake-utils.lib.eachDefaultSystem (system: { self, nixpkgs, ... }:
let let
overlays = [ rust-overlay.overlays.default ]; supportedSystems = [
pkgs = import nixpkgs { "x86_64-linux"
inherit system overlays; # "aarch64-linux"
}; # "x86_64-darwin"
rust = pkgs.rust-bin.stable.latest.default; # "aarch64-darwin"
openssl = pkgs.openssl; ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in in
{ {
packages.default = pkgs.rustPlatform.buildRustPackage { packages = forAllSystems (
pname = "mycoolwebsite"; system:
version = "0.1.1"; let
pkgs = import nixpkgs { inherit system; };
in
{
default = pkgs.rustPlatform.buildRustPackage {
pname = "quick-rust-website";
version = "0.1.0";
src = ./.; src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
nativeBuildInputs = [ cargoLock.lockFile = ./Cargo.lock;
rust
pkgs.pkg-config nativeBuildInputs = [ pkgs.pkg-config ];
];
buildInputs = [ buildInputs = [
openssl pkgs.openssl
# pkgs.sqlite
]; ];
OPENSSL_LIB_DIR = "${openssl.out}/lib"; PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
OPENSSL_INCLUDE_DIR = "${openssl.dev}/include"; OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
PKG_CONFIG_PATH = "${openssl.dev}/lib/pkgconfig"; OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include";
}; };
}
);
devShells.default = pkgs.mkShell { devShells = forAllSystems (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
default = pkgs.mkShell {
packages = [ packages = [
rust pkgs.rustc
pkgs.cargo pkgs.cargo
pkgs.rust-analyzer pkgs.rust-analyzer
pkgs.pkg-config pkgs.pkg-config
openssl pkgs.openssl
]; ];
OPENSSL_DIR = openssl.dev;
PKG_CONFIG_PATH = "${openssl.dev}/lib/pkgconfig";
};
});
}
env = {
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
OPENSSL_DIR = "${pkgs.openssl.dev}";
};
};
}
);
nixosModules.default =
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.slimes-server;
in
{
options.services.slimes-server = {
enable = lib.mkEnableOption "Quick rust website template";
port = lib.mkOption {
type = lib.types.port;
default = 9003;
};
# databasePath = lib.mkOption {
# type = lib.types.str;
# default = "/var/lib/quick-rust-website/database.db";
# };
};
config = lib.mkIf cfg.enable {
systemd.services.slimes-server = {
description = "Quick rust website template";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${
self.packages.${pkgs.system}.default
}/bin/quick-rust-website --database-url ${cfg.databasePath} --port ${toString cfg.port}";
Restart = "on-failure";
StateDirectory = "quick-rust-website";
DynamicUser = true;
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
};
};
};
};
};
}

View File

@@ -1,19 +1,55 @@
use askama::Template; use std::sync::Arc;
use axum::{ use axum::{
http::StatusCode, extract::State,
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use serde::Serialize;
use tera::Context;
pub async fn render_homepage() -> impl IntoResponse { use crate::{AppState, TEMPLATES};
#[derive(Template)]
#[template(path = "homepage.html")]
struct HomePageTemplate<'a> {
lang: &'a str,
}
let tmpl = HomePageTemplate { lang: "en" }; /// Handler for the Home Page
match tmpl.render() { pub async fn home_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let mut context = Context::new();
context.insert("title", &state.app_name);
context.insert("server_time", &chrono::Local::now().to_rfc2822());
match TEMPLATES.render("index.html", &context) {
Ok(html) => Html(html).into_response(), Ok(html) => Html(html).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Template render error").into_response(), Err(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
/// Handler for the Showcase Page
pub async fn showcase_handler() -> impl IntoResponse {
#[derive(Serialize)]
struct ShowcaseItem {
name: String,
description: String,
}
let mut context = Context::new();
context.insert("title", "Showcase");
let items = vec![
ShowcaseItem {
name: "Project Alpha".into(),
description: "A cool Rust project".into(),
},
ShowcaseItem {
name: "Project Beta".into(),
description: "An Axum powered API".into(),
},
ShowcaseItem {
name: "Project Gamma".into(),
description: "Tera template engine".into(),
},
];
context.insert("items", &items);
match TEMPLATES.render("showcase.html", &context) {
Ok(html) => Html(html).into_response(),
Err(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }

View File

@@ -1,59 +0,0 @@
use std::{net::SocketAddr, time::Duration};
use axum::{
Router,
http::{Method, header},
routing::get,
};
use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder};
use tower_http::cors::{Any, CorsLayer};
use crate::handlers::render_homepage;
pub mod handlers;
/// Start the server with the given configuration and output file.
/// `addr` should be something like "127.0.0.1:8081"
pub async fn run_server(addr: &str, verbose: bool) -> anyhow::Result<()> {
// CORS
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]);
// rate limiter
let governor_conf = GovernorConfigBuilder::default()
.per_second(3)
.burst_size(10)
.finish()
.unwrap();
// a separate background task to clean up
let governor_limiter = governor_conf.limiter().clone();
let interval = Duration::from_secs(60);
std::thread::spawn(move || {
loop {
std::thread::sleep(interval);
if verbose {
tracing::info!("rate limiting storage size: {}", governor_limiter.len());
}
governor_limiter.retain_recent();
}
});
let app = Router::new()
.route("/", get(render_homepage))
.layer(cors)
.layer(GovernorLayer::new(governor_conf));
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("Listening on {}", addr);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}

View File

@@ -1,26 +1,55 @@
use clap::Parser; use axum::{Router, routing::get};
use lazy_static::lazy_static;
use std::sync::Arc;
use tera::Tera;
use mycoolwebsite::run_server; mod handlers;
use handlers::{home_handler, showcase_handler};
#[derive(Parser, Debug)] lazy_static! {
#[command(author, version, about, long_about = None)] pub static ref TEMPLATES: Tera = {
struct Cli { let mut tera = Tera::default();
#[arg(short, long)] tera.add_raw_templates(vec![
verbose: bool, ("_layout.html", include_str!("../templates/_layout.html")),
("_base.css", include_str!("../templates/_base.css")),
("index.html", include_str!("../templates/homepage.html")),
("showcase.html", include_str!("../templates/showcase.html")),
])
.unwrap();
tera
};
}
struct AppState {
app_name: String,
} }
/// Cli handler
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let cli = Cli::parse(); tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let subscriber = tracing_subscriber::FmtSubscriber::new(); let shared_state = Arc::new(AppState {
tracing::subscriber::set_global_default(subscriber).ok(); app_name: "Quick rust website template".to_string(),
});
let port = std::env::var("SERVER_PORT").unwrap_or("8081".to_string()); let app = Router::new()
let addr = format!("127.0.0.1:{port}"); .route("/", get(home_handler))
.route("/showcase", get(showcase_handler))
.with_state(shared_state);
run_server(&addr, cli.verbose).await?; let host = false;
let port = "8000";
let addr = if host {
format!("0.0.0.0:{}", port)
} else {
format!("127.0.0.1:{}", port)
};
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Listening on http://{}", addr);
axum::serve(listener, app).await?;
Ok(()) Ok(())
} }

60
templates/_base.css Normal file
View File

@@ -0,0 +1,60 @@
* {
box-sizing: border-box;
}
html {
font-size: 15px;
}
body {
font-family:
"Inter",
system-ui,
-apple-system,
Roboto,
"Segoe UI",
Arial,
sans-serif;
padding: 20px;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
overflow-x: hidden;
height: 100vh;
}
h2 {
font-size: 1.8rem;
margin-bottom: 15px;
}
nav {
padding: 1rem;
}
nav a {
margin-right: 15px;
text-decoration: none;
}
main {
padding: 2rem;
flex: 1;
max-width: 800px;
margin: 0 auto;
}
footer {
padding: 1rem;
text-align: center;
}
.card {
border: 1px solid #ddd;
padding: 10px;
border-radius: 8px;
margin-top: 10px;
}

View File

@@ -1,99 +0,0 @@
html {
font-size: 15px;
}
body {
font-family: 'Inter', system-ui, -apple-system, Roboto, "Segoe UI", Arial, sans-serif;
padding: 20px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
overflow-x: hidden;
}
h2 {
font-size: 1.8rem;
margin-bottom: 15px;
}
.container {
display: flex;
flex-direction: row;
text-align: left;
justify-content: center;
align-items: center;
background: #fff;
border-radius: 16px;
padding: 2rem;
max-width: 720px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
img {
display: block;
margin: 0 auto;
}
label {
font-weight: 600;
display: block;
margin-bottom: 0.25rem;
font-size: 1.35rem;
color: #222;
}
input,
textarea,
select {
width: 100%;
padding: 0.6rem 0.75rem;
margin-bottom: 1rem;
border: none;
border-radius: 8px;
box-sizing: border-box;
font-size: 1rem;
color: #333333aa;
background-color: #e0f0ff;
border: 1px solid #88c1ff;
transition: all 0.2s ease-in-out;
}
input:focus,
textarea:focus,
select:focus {
outline: #66aaffaa;
background-color: #cce5ff;
}
textarea {
resize: none;
}
button {
padding: 0.5rem 1rem;
font-size: 1.3rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
background-color: #66aaff;
color: #fff;
transition: background-color 0.2s;
transition: color 0.2s;
}
button:hover {
background-color: #3390ff;
}
@media (max-width: 720px) {
html {
font-size: 20px;
}
.container {
max-width: 100%;
width: 100%;
}
}

View File

@@ -1,16 +1,20 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ lang }}"> <html>
<head> <head>
<meta charset="utf-8"/> <title>{{ title }}</title>
<title>{% block title %}Dynamic Form{% endblock %}</title> <link rel="stylesheet" href="_base.css">
<style> <style>
/*<![CDATA[*/ {% include "_base.css" %}
{%~ include "_layout.css" ~%}
/*]]>*/
</style> </style>
</head> </head>
<body> <body>
<nav>
<a href="/">Home</a>
<a href="/showcase">Showcase</a>
</nav>
<main>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main>
<footer>Quick rust website template</footer>
</body> </body>
</html> </html>

View File

@@ -1,9 +1,6 @@
{% extends "_layout.html" %} {% extends "_layout.html" %}
{% block title %}Home Page{% endblock %}
{% block content %} {% block content %}
<div class="container"> <h1>Welcome to the Home Page</h1>
<p>Hello world!</p> <p>This is a starter website built with Axum and Tera.</p>
</div> <p>The time on the server is: <strong>{{ server_time }}</strong></p>
{% endblock %} {% endblock %}

12
templates/showcase.html Normal file
View File

@@ -0,0 +1,12 @@
{% extends "_layout.html" %}
{% block content %}
<h1>Project Showcase</h1>
<p>Here are some items passed from Rust to the template:</p>
{% for item in items %}
<div class="card">
<h3>{{ item.name }}</h3>
<p>{{ item.description }}</p>
</div>
{% endfor %}
{% endblock %}