diff --git a/Justfile b/Justfile index 64db1d01..4dd260c3 100644 --- a/Justfile +++ b/Justfile @@ -1,8 +1,8 @@ -watch-rs: +watch-site: bacon run -- --manifest-path rs/Cargo.toml --package site watch-clippy: - bacon clippy -- --manifest-path rs/Cargo.toml --package site + bacon clippy -- --manifest-path rs/Cargo.toml --all setup: cp .env.template .env @@ -13,9 +13,6 @@ run-docker: docker load -i $(nix build .#packages.aarch64-linux.site-image --print-out-paths) docker run --rm -e PORT=8080 -p 8080:8080 site:latest -run-rs: - cargo run --manifest-path rs/Cargo.toml - rb: just nice just check @@ -35,7 +32,7 @@ format-nix: treefmt format-rs: - cargo fmt --manifest-path rs/Cargo.toml + cargo fmt --manifest-path rs/Cargo.toml --all format-tf: tofu -chdir=tf fmt @@ -52,7 +49,7 @@ lint-nix: deadnix --edit lint-rs: - cargo clippy --manifest-path rs/Cargo.toml --fix --allow-dirty + cargo clippy --manifest-path rs/Cargo.toml --fix --allow-dirty --all test: test-nix test-rs @@ -60,7 +57,7 @@ test-nix: nix flake check --all-systems test-rs: - cargo test --manifest-path rs/Cargo.toml + cargo test --manifest-path rs/Cargo.toml --all check: check-gha check-lua check-nix check-rs test @@ -76,5 +73,5 @@ check-nix: deadnix check-rs: - cargo clippy --manifest-path rs/Cargo.toml - cargo fmt --manifest-path rs/Cargo.toml --check + cargo clippy --manifest-path rs/Cargo.toml --all + cargo fmt --manifest-path rs/Cargo.toml --check --all diff --git a/rs/Cargo.lock b/rs/Cargo.lock index a68f7c11..41474166 100644 --- a/rs/Cargo.lock +++ b/rs/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "askama" version = "0.15.1" @@ -69,6 +78,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.8" @@ -146,18 +161,53 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -199,6 +249,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -241,6 +303,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "globset" version = "0.4.18" @@ -342,6 +416,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ignore" version = "0.4.25" @@ -374,12 +472,39 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lib" +version = "1.0.0" +dependencies = [ + "chrono", + "ignore", + "markdown-frontmatter", + "serde", + "tempfile", +] + [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "lock_api" version = "0.4.14" @@ -446,6 +571,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -511,6 +645,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -543,6 +683,25 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.22" @@ -652,6 +811,12 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -668,7 +833,9 @@ version = "0.1.0-alpha" dependencies = [ "askama", "axum", + "chrono", "ignore", + "lib", "markdown", "markdown-frontmatter", "serde", @@ -708,6 +875,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.17" @@ -877,6 +1057,60 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -886,12 +1120,65 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -984,6 +1271,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "zmij" version = "1.0.13" diff --git a/rs/Cargo.toml b/rs/Cargo.toml index fcf6460b..4f0f0b04 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["site"] +members = ["lib","site"] diff --git a/rs/lib/Cargo.toml b/rs/lib/Cargo.toml new file mode 100644 index 00000000..3d6e20a3 --- /dev/null +++ b/rs/lib/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "lib" +version = "1.0.0" +description = "Catch-all for helpers" +readme = "../../README.md" +repository = "https://github.com/rrvsh/tools" +edition = "2024" +license = "MIT" +keywords = ["lib"] +categories = ["lib"] + +[dependencies] +chrono = "0.4.43" +ignore = "0.4.25" +markdown-frontmatter = { version = "0.4.0", features = ["yaml"] } +serde = "1.0.228" +tempfile = "3.24.0" diff --git a/rs/lib/src/lib.rs b/rs/lib/src/lib.rs new file mode 100644 index 00000000..0f75a02c --- /dev/null +++ b/rs/lib/src/lib.rs @@ -0,0 +1,119 @@ +use chrono::NaiveDate; +use ignore::Walk; +use serde::Deserialize; +use std::ffi::OsStr; +use std::path::Path; + +#[derive(Clone, PartialEq, Debug)] +pub struct Document { + pub title: String, + pub slug: String, + pub date: NaiveDate, + pub content: String, +} + +#[derive(Deserialize)] +struct ArticleFrontmatter { + title: Option, + slug: Option, + date: Option, +} + +impl Document { + pub fn from_path>(path: P) -> Option { + let path = path.as_ref(); + let is_markdown = path.extension() == Some(OsStr::new("md")); + if !path.is_file() && !is_markdown { + return None; + } + let file_content = std::fs::read_to_string(path).ok()?; + let (frontmatter, content) = + markdown_frontmatter::parse::(&file_content).ok()?; + let date = frontmatter.date?; + let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").ok()?; + let slug = frontmatter.slug?; + let title = frontmatter.title?; + Some(Self { + title, + slug, + date, + content: content.trim().to_string(), + }) + } +} + +pub fn load_documents_from_dir>(path: P) -> Vec { + let mut documents = Walk::new(path) + .filter_map(|entry| entry.ok()) + .filter_map(|entry| Document::from_path(entry.path())) + .collect::>(); + documents.sort_by(|left, right| right.date.cmp(&left.date)); + documents +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn document_success() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("my-temporary-note.md"); + let mut file = File::create(&file_path).unwrap(); + let doc = r#"--- +title: Test +slug: test +date: 2026-01-21 +--- +Testing..."#; + writeln!(file, "{}", doc).unwrap(); + let actual = Document::from_path(&file_path); + let expected = Some(Document { + title: "Test".to_string(), + slug: "test".to_string(), + date: NaiveDate::from_ymd_opt(2026, 1, 21).unwrap(), + content: "Testing...".to_string(), + }); + dbg!(&actual, &expected); + assert!(actual == expected); + } + + #[test] + fn document_failure_not_markdown() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("my-temporary-note.txt"); + let _ = File::create(&file_path).unwrap(); + let actual = Document::from_path(&file_path); + let expected = None; + assert!(actual == expected); + } + + #[test] + fn document_failure_not_file() { + let dir = tempdir().unwrap(); + let file_path = dir.path(); + let actual = Document::from_path(file_path); + let expected = None; + assert!(actual == expected); + } + + #[test] + fn document_failure_invalid_date() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("my-temporary-note.md"); + let mut file = File::create(&file_path).unwrap(); + let doc = r#"--- + title: Test + slug: test + date: not-a-date + --- + Testing..."#; + writeln!(file, "{}", doc).unwrap(); + let actual = Document::from_path(&file_path); + let expected = None; + assert!(actual == expected); + } +} diff --git a/rs/site/Cargo.toml b/rs/site/Cargo.toml index 143a6088..5885d75c 100644 --- a/rs/site/Cargo.toml +++ b/rs/site/Cargo.toml @@ -12,7 +12,9 @@ categories = ["site"] [dependencies] askama = "0.15.1" axum = "0.8.8" +chrono = "0.4.43" ignore = "0.4.25" +lib = { version = "1.0.0", path = "../lib" } markdown = "1.0.0" markdown-frontmatter = { version = "0.4.0", features = ["yaml"] } serde = "1.0.228" diff --git a/rs/site/src/app/mod.rs b/rs/site/src/app/mod.rs index 8141c860..6c981aeb 100644 --- a/rs/site/src/app/mod.rs +++ b/rs/site/src/app/mod.rs @@ -1,10 +1,15 @@ +use std::sync::Arc; + pub mod routes; pub mod settings; +pub mod state; pub async fn serve() { let settings = settings::AppSettings::from_env(); + let documents = lib::load_documents_from_dir(&settings.content_dir); + let state = Arc::new(state::AppState::new(documents)); let listener = tokio::net::TcpListener::bind(&settings.addr).await.unwrap(); - let router = routes::build_router(settings); + let router = routes::build_router(state); axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) .await diff --git a/rs/site/src/app/routes.rs b/rs/site/src/app/routes.rs index 65c0f55b..a07ed3c8 100644 --- a/rs/site/src/app/routes.rs +++ b/rs/site/src/app/routes.rs @@ -1,4 +1,4 @@ -use crate::app::settings::AppSettings; +use crate::app::state::{AppState, MonthGroup}; use askama::Template; use axum::{ extract::{Path, State}, @@ -6,36 +6,29 @@ use axum::{ response::{Html, IntoResponse, Response}, routing::get, }; -use ignore::{DirEntry, WalkBuilder}; +use chrono::{Datelike, NaiveDate}; use markdown::to_html; -use serde::Deserialize; -use std::fs::read_to_string; +use std::sync::Arc; -pub fn build_router(settings: AppSettings) -> axum::Router { +pub fn build_router(state: Arc) -> axum::Router { axum::Router::new() .route("/", get(index_get)) - .route("/blog", get(blog_index_get)) .route("/{year}/{month}/{day}/{slug}", get(article_get)) - .with_state(settings) + .with_state(state) } #[derive(Template)] #[template(path = "index.html")] -struct IndexTemplate {} - -pub async fn index_get() -> Result { - IndexTemplate {}.render().map_or_else( - |_| Err(StatusCode::INTERNAL_SERVER_ERROR), - |rendered| Ok(Html(rendered).into_response()), - ) +struct IndexTemplate { + months: Vec, } -#[derive(Template)] -#[template(path = "blog/index.html")] -struct BlogIndexTemplate {} - -pub async fn blog_index_get() -> Result { - BlogIndexTemplate {}.render().map_or_else( +pub async fn index_get(State(state): State>) -> Result { + IndexTemplate { + months: state.months.clone(), + } + .render() + .map_or_else( |_| Err(StatusCode::INTERNAL_SERVER_ERROR), |rendered| Ok(Html(rendered).into_response()), ) @@ -44,70 +37,48 @@ pub async fn blog_index_get() -> Result { #[derive(Template)] #[template(path = "article.html")] struct ArticleTemplate { + title: String, + date: String, content: String, } -#[derive(Deserialize)] -struct ArticleFrontmatter { - slug: String, -} - -fn is_requested_article(entry: &DirEntry, slug: &str) -> bool { - let file_extension = entry - .path() - .extension() - .map(|osstr| osstr.to_str().expect("Invalid UTF-8!")); - if Some("md") == file_extension { - let file_content = read_to_string(entry.path()).expect("Error reading file to string!"); - let (frontmatter, _) = markdown_frontmatter::parse::(&file_content) - .expect("Error parsing frontmatter!"); - frontmatter.slug == slug - } else { - false - } -} +fn format_article_date(date: NaiveDate) -> String { + let day = date.day(); + let suffix = match day { + 11..=13 => "th", + _ => match day % 10 { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + }, + }; -fn get_first_file_path_by_slug(folder_path: &str, slug: &str) -> Option { - let matching_entries = WalkBuilder::new(folder_path) - .max_depth(Some(1)) - .build() - .filter_map(|entry| entry.ok().filter(|entry| is_requested_article(entry, slug))) - .collect::>(); - if matching_entries.len() == 1 { - Some( - matching_entries - .first() - .expect("Error getting dir entry!") - .path() - .to_str() - .expect("Invalid UTF-8") - .to_string(), - ) - } else { - None - } + format!( + "written on {}, the {day}{suffix} of {} {}", + date.format("%A"), + date.format("%B"), + date.year() + ) } pub async fn article_get( - Path((year, month, day, slug)): Path<(i32, i32, i32, String)>, - State(settings): State, + Path((year, month, day, slug)): Path<(i32, u32, u32, String)>, + State(state): State>, ) -> Result { - let AppSettings { content_dir, .. } = settings; - let folder_path = format!("{content_dir}/{year}/{month}/{day}"); - get_first_file_path_by_slug(&folder_path, &slug).map_or_else( - || Err(StatusCode::NOT_FOUND), - |file_path| { - let file_content = read_to_string(file_path).expect("Error reading file!"); - let (_, body) = markdown_frontmatter::parse::(&file_content) - .expect("Error parsing frontmatter!"); - ArticleTemplate { - content: to_html(body), - } - .render() - .map_or_else( - |_| Err(StatusCode::INTERNAL_SERVER_ERROR), - |rendered| Ok(Html(rendered).into_response()), - ) - }, + let requested_date = NaiveDate::from_ymd_opt(year, month, day).ok_or(StatusCode::NOT_FOUND)?; + let document = state + .articles_by_key + .get(&(year, month, day, slug)) + .ok_or(StatusCode::NOT_FOUND)?; + ArticleTemplate { + title: document.title.clone(), + date: format_article_date(requested_date), + content: to_html(&document.content), + } + .render() + .map_or_else( + |_| Err(StatusCode::INTERNAL_SERVER_ERROR), + |rendered| Ok(Html(rendered).into_response()), ) } diff --git a/rs/site/src/app/state.rs b/rs/site/src/app/state.rs new file mode 100644 index 00000000..c1ccee96 --- /dev/null +++ b/rs/site/src/app/state.rs @@ -0,0 +1,87 @@ +use chrono::{Datelike, NaiveDate}; +use lib::Document; +use std::collections::{BTreeMap, HashMap}; + +#[derive(Clone)] +pub struct ArticleLink { + pub title: String, + pub url: String, + pub date: NaiveDate, + pub content: String, +} + +#[derive(Clone)] +pub struct MonthGroup { + pub label: String, + pub articles: Vec, +} + +#[derive(Clone)] +pub struct AppState { + pub months: Vec, + pub articles_by_key: HashMap<(i32, u32, u32, String), ArticleLink>, +} + +impl AppState { + pub fn new(documents: Vec) -> Self { + let mut grouped: BTreeMap<(i32, u32), Vec> = BTreeMap::new(); + let mut articles_by_key = HashMap::new(); + + for document in documents { + grouped + .entry((document.date.year(), document.date.month())) + .or_default() + .push(document); + } + + let months = grouped + .into_iter() + .rev() // btreemap sorts ascending + .map(|((year, month), documents)| { + let mut articles = documents + .into_iter() + .map(|document| { + let date = document.date; + let slug = document.slug; + let article = ArticleLink { + title: document.title, + url: format!( + "/{}/{:02}/{:02}/{}", + date.year(), + date.month(), + date.day(), + slug + ), + date, + content: document.content, + }; + articles_by_key.insert( + (date.year(), date.month(), date.day(), slug), + article.clone(), + ); + article + }) + .collect::>(); + + articles.sort_by(|left, right| { + left.date + .cmp(&right.date) + .then_with(|| right.title.cmp(&left.title)) + }); + + MonthGroup { + label: NaiveDate::from_ymd_opt(year, month, 1).map_or_else( + || format!("{month:02} {year}"), + |date| date.format("%B %Y").to_string(), + ), + articles, + } + }) + .collect(); + + Self { + months, + articles_by_key, + } + } +} diff --git a/rs/site/templates/article.html b/rs/site/templates/article.html index 2b20a646..de572c3d 100644 --- a/rs/site/templates/article.html +++ b/rs/site/templates/article.html @@ -1,5 +1,5 @@ -{% extends "base.html" %} - -{% block content %} - {{ content | safe }} +{% extends "base.html" %} {% block content %} +

{{ title }}

+{{ date }} +
{{ content | safe }}
{% endblock %} diff --git a/rs/site/templates/base.html b/rs/site/templates/base.html index eb8be6d1..c6d4271f 100644 --- a/rs/site/templates/base.html +++ b/rs/site/templates/base.html @@ -1,22 +1,17 @@ - + -
- {% block header %} - {% endblock %} -
-
- {% block content %} - {% endblock %} -
+
{% block header %} {% endblock %}
+
{% block content %} {% endblock %}
diff --git a/rs/site/templates/blog/index.html b/rs/site/templates/blog/index.html deleted file mode 100644 index a17acd73..00000000 --- a/rs/site/templates/blog/index.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

blog index page

-

currently under construction!

-

follow progress here

-{% endblock %} diff --git a/rs/site/templates/index.html b/rs/site/templates/index.html index bd400c44..caf30e72 100644 --- a/rs/site/templates/index.html +++ b/rs/site/templates/index.html @@ -1,7 +1,18 @@ -{% extends "base.html" %} - -{% block content %} -

rrv.sh index page

-

currently under construction!

-

follow progress here

-{% endblock %} +{% extends "base.html" %} {% block content %} +

rrv.sh index page

+

+ currently under construction! follow progress + here +

+

my links

+GitHub +Lichess +

my prose

+{% for month in months %} +

{{ month.label }}

+ +{% endfor %} {% endblock %}