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]
name = "mycoolwebsite"
name = "quick-rust-website"
version = "0.1.1"
edition = "2024"
@@ -9,8 +9,10 @@ tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.43"
tracing-subscriber = "0.3.22"
tower-http = { version = "0.6.6", features = ["cors", "limit"] }
tower_governor = "0.8.0"
anyhow = "1.0.100"
chrono = { version = "0.4.42", features = ["serde"] }
askama = "0.14.0"
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": {
"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": {
"locked": {
"lastModified": 1764950072,
@@ -34,60 +16,9 @@
"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": {
"inputs": {
"flake-utils": "flake-utils",
"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"
"nixpkgs": "nixpkgs"
}
}
},

123
flake.nix
View File

@@ -1,57 +1,114 @@
{
description = "My cool website";
description = "Quick rust website template";
inputs = {
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, ... }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
{ self, nixpkgs, ... }:
let
overlays = [ rust-overlay.overlays.default ];
pkgs = import nixpkgs {
inherit system overlays;
};
rust = pkgs.rust-bin.stable.latest.default;
openssl = pkgs.openssl;
supportedSystems = [
"x86_64-linux"
# "aarch64-linux"
# "x86_64-darwin"
# "aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in
{
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "mycoolwebsite";
version = "0.1.1";
packages = forAllSystems (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
default = pkgs.rustPlatform.buildRustPackage {
pname = "quick-rust-website";
version = "0.1.0";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
nativeBuildInputs = [
rust
pkgs.pkg-config
];
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [
openssl
pkgs.openssl
# pkgs.sqlite
];
OPENSSL_LIB_DIR = "${openssl.out}/lib";
OPENSSL_INCLUDE_DIR = "${openssl.dev}/include";
PKG_CONFIG_PATH = "${openssl.dev}/lib/pkgconfig";
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
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 = [
rust
pkgs.rustc
pkgs.cargo
pkgs.rust-analyzer
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::{
http::StatusCode,
extract::State,
response::{Html, IntoResponse},
};
use serde::Serialize;
use tera::Context;
pub async fn render_homepage() -> impl IntoResponse {
#[derive(Template)]
#[template(path = "homepage.html")]
struct HomePageTemplate<'a> {
lang: &'a str,
}
use crate::{AppState, TEMPLATES};
let tmpl = HomePageTemplate { lang: "en" };
match tmpl.render() {
/// Handler for the Home Page
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(),
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)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
lazy_static! {
pub static ref TEMPLATES: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("_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]
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();
tracing::subscriber::set_global_default(subscriber).ok();
let shared_state = Arc::new(AppState {
app_name: "Quick rust website template".to_string(),
});
let port = std::env::var("SERVER_PORT").unwrap_or("8081".to_string());
let addr = format!("127.0.0.1:{port}");
let app = Router::new()
.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(())
}

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

View File

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