From 4504a88f4b35fd0d69953c0089295245af372f85 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Tue, 4 Nov 2025 08:55:14 -0800 Subject: [PATCH] `Page` abstraction --- Cargo.lock | 130 ++++++++++- Cargo.toml | 4 + crates/bin/webpage/src/cmd/serve.rs | 24 +- crates/lib/libservice/Cargo.toml | 1 + crates/lib/libservice/src/lib.rs | 24 +- crates/service/service-webpage/Cargo.toml | 4 +- .../service-webpage/src/components/base.rs | 86 ------- .../service-webpage/src/components/misc.rs | 7 +- .../service-webpage/src/components/mod.rs | 1 - crates/service/service-webpage/src/lib.rs | 8 +- .../service/service-webpage/src/page/mod.rs | 216 ++++++++++++++++++ .../src/{routes => pages}/betalupi.rs | 38 ++- .../src/{routes => pages}/handouts.rs | 37 ++- .../src/{routes => pages}/index.md | 0 .../src/{routes => pages}/index.rs | 31 ++- .../src/{routes => pages}/links.md | 0 .../service-webpage/src/pages/links.rs | 35 +++ .../service/service-webpage/src/pages/mod.rs | 4 + .../service-webpage/src/routes/links.rs | 37 --- .../service/service-webpage/src/routes/mod.rs | 68 ++++-- 20 files changed, 522 insertions(+), 233 deletions(-) delete mode 100644 crates/service/service-webpage/src/components/base.rs create mode 100644 crates/service/service-webpage/src/page/mod.rs rename crates/service/service-webpage/src/{routes => pages}/betalupi.rs (77%) rename crates/service/service-webpage/src/{routes => pages}/handouts.rs (95%) rename crates/service/service-webpage/src/{routes => pages}/index.md (100%) rename crates/service/service-webpage/src/{routes => pages}/index.rs (76%) rename crates/service/service-webpage/src/{routes => pages}/links.md (100%) create mode 100644 crates/service/service-webpage/src/pages/links.rs create mode 100644 crates/service/service-webpage/src/pages/mod.rs delete mode 100644 crates/service/service-webpage/src/routes/links.rs diff --git a/Cargo.lock b/Cargo.lock index cb6cada..f42992a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[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 = "anstream" version = "0.6.21" @@ -275,6 +284,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.51" @@ -353,6 +375,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -522,6 +550,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -641,6 +675,11 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -766,6 +805,30 @@ dependencies = [ "tracing", ] +[[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 = "icu_collections" version = "2.1.1" @@ -963,6 +1026,7 @@ name = "libservice" version = "0.0.1" dependencies = [ "axum", + "tokio", "tower-http", "tracing", "utoipa", @@ -1024,6 +1088,15 @@ dependencies = [ "prost-types", ] +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.0", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1918,15 +1991,17 @@ version = "0.0.1" dependencies = [ "assetserver", "axum", + "chrono", "emojis", "libservice", + "lru", "macro-assets", "macro-sass", "markdown-it", "maud", + "parking_lot", "strum", "tracing", - "utoipa", ] [[package]] @@ -2710,12 +2785,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 2.0.108", +] + +[[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 2.0.108", +] + [[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.52.0" diff --git a/Cargo.toml b/Cargo.toml index 79dd11a..fd16423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,10 @@ itertools = "0.14.0" anyhow = "1.0.97" url = { version = "2.5.7", features = ["serde"] } num = "0.4.3" +chrono = "0.4.42" +lru = "0.16.2" +parking_lot = "0.12.5" + # # Macro utilities diff --git a/crates/bin/webpage/src/cmd/serve.rs b/crates/bin/webpage/src/cmd/serve.rs index a54b981..71f0fcb 100644 --- a/crates/bin/webpage/src/cmd/serve.rs +++ b/crates/bin/webpage/src/cmd/serve.rs @@ -1,9 +1,7 @@ use anyhow::{Context, Result}; -use axum::{extract::connect_info::Connected, serve::IncomingStream}; -use libservice::{Service, ToService}; +use libservice::{Service, ServiceConnectInfo, ToService}; use service_webpage::WebpageService; -use std::{net::SocketAddr, sync::Arc}; -use tokio::net::TcpListener; +use std::sync::Arc; use tracing::{error, info}; use crate::CmdContext; @@ -25,7 +23,7 @@ impl ServeArgs { .context("while building service")? .make_router() .expect("service must be initialized") - .into_make_service_with_connect_info::(); + .into_make_service_with_connect_info::(); let listener = match tokio::net::TcpListener::bind(self.addr.clone()).await { Ok(x) => x, @@ -75,19 +73,3 @@ pub async fn make_service(_state: Option>) -> Result, -} - -impl Connected> for ServerConnectInfo { - fn connect_info(target: IncomingStream<'_, TcpListener>) -> Self { - let addr = target.remote_addr(); - - Self { - addr: Arc::new(*addr), - } - } -} diff --git a/crates/lib/libservice/Cargo.toml b/crates/lib/libservice/Cargo.toml index 3f40782..cea99a9 100644 --- a/crates/lib/libservice/Cargo.toml +++ b/crates/lib/libservice/Cargo.toml @@ -13,3 +13,4 @@ tracing = { workspace = true } tower-http = { workspace = true } utoipa = { workspace = true } utoipa-swagger-ui = { workspace = true } +tokio = { workspace = true } diff --git a/crates/lib/libservice/src/lib.rs b/crates/lib/libservice/src/lib.rs index 45bef33..fad2c41 100644 --- a/crates/lib/libservice/src/lib.rs +++ b/crates/lib/libservice/src/lib.rs @@ -1,5 +1,4 @@ //! Abstractions for modular http API routes - use axum::{ Json, Router, extract::{Request, State}, @@ -7,7 +6,10 @@ use axum::{ middleware::Next, response::{IntoResponse, Response}, }; +use axum::{extract::connect_info::Connected, serve::IncomingStream}; use std::ops::Deref; +use std::{net::SocketAddr, sync::Arc}; +use tokio::net::TcpListener; use tower_http::trace::TraceLayer; use tracing::info; use utoipa::openapi::{ @@ -16,6 +18,22 @@ use utoipa::openapi::{ }; use utoipa_swagger_ui::SwaggerUi; +/// For use with `into_make_service_with_connect_info` +#[derive(Clone, Debug)] +pub struct ServiceConnectInfo { + pub addr: Arc, +} + +impl Connected> for ServiceConnectInfo { + fn connect_info(target: IncomingStream<'_, TcpListener>) -> Self { + let addr = target.remote_addr(); + + Self { + addr: Arc::new(*addr), + } + } +} + /// A `Service` provides a set of api endpoints and docs. /// This has no relation to [tower::Service]. pub trait ToService @@ -26,7 +44,9 @@ where fn make_router(&self) -> Option>; /// Create an openapi spec for this service - fn make_openapi(&self) -> OpenApi; + fn make_openapi(&self) -> OpenApi { + OpenApi::default() + } /// Get the service name for grouping endpoints fn service_name(&self) -> Option { diff --git a/crates/service/service-webpage/Cargo.toml b/crates/service/service-webpage/Cargo.toml index 35cc89c..9d99516 100644 --- a/crates/service/service-webpage/Cargo.toml +++ b/crates/service/service-webpage/Cargo.toml @@ -15,8 +15,10 @@ assetserver = { workspace = true } axum = { workspace = true } tracing = { workspace = true } -utoipa = { workspace = true } maud = { workspace = true } markdown-it = { workspace = true } emojis = { workspace = true } strum = { workspace = true } +chrono = { workspace = true } +parking_lot = { workspace = true } +lru = { workspace = true } diff --git a/crates/service/service-webpage/src/components/base.rs b/crates/service/service-webpage/src/components/base.rs deleted file mode 100644 index a91b63e..0000000 --- a/crates/service/service-webpage/src/components/base.rs +++ /dev/null @@ -1,86 +0,0 @@ -use assetserver::Asset; -use maud::{DOCTYPE, Markup, PreEscaped, Render, html}; - -use crate::{components::misc::FarLink, routes::assets::Styles_Main}; - -pub struct PageMetadata { - pub title: String, - pub author: Option, - pub description: Option, - pub image: Option, -} - -impl Render for PageMetadata { - fn render(&self) -> Markup { - let empty = String::new(); - let title = &self.title; - let author = &self.author.as_ref().unwrap_or(&empty); - let description = &self.description.as_ref().unwrap_or(&empty); - let image = &self.image.as_ref().unwrap_or(&empty); - - html!( - meta property="og:site_name" content=(title) {} - meta name="title" content=(title) {} - meta property="og:title" content=(title) {} - meta property="twitter:title" content=(title) {} - - meta name="author" content=(author) {} - - meta name="description" content=(description) {} - meta property="og:description" content=(description) {} - meta property="twitter:description" content=(description) {} - - meta content=(image) property="og:image" {} - link rel="shortcut icon" href=(image) type="image/x-icon" {} - ) - } -} - -pub struct BasePage(pub PageMetadata, pub T); - -impl Render for BasePage { - fn render(&self) -> Markup { - let meta = &self.0; - - html! { - (DOCTYPE) - html { - head { - meta charset="UTF" {} - meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {} - meta content="text/html; charset=UTF-8" http-equiv="content-type" {} - meta property="og:type" content="website" {} - - link rel="stylesheet" href=(Styles_Main::URL) {} - - - (meta) - title { (PreEscaped(meta.title.clone())) } - } - - body { - div class="wrapper" { - main { (self.1) } - - footer { - hr class = "footline" {} - div class = "footContainer" { - p { - "This site was built by hand using " - (FarLink("https://rust-lang.org", "Rust")) - ", " - (FarLink("https://maud.lambda.xyz", "Maud")) - ", " - (FarLink("https://github.com/connorskees/grass", "Grass")) - ", and " - (FarLink("https://docs.rs/axum/latest/axum", "Axum")) - "." - } - } - } - } - } - } - } - } -} diff --git a/crates/service/service-webpage/src/components/misc.rs b/crates/service/service-webpage/src/components/misc.rs index 77523e2..f6c0c8d 100644 --- a/crates/service/service-webpage/src/components/misc.rs +++ b/crates/service/service-webpage/src/components/misc.rs @@ -16,12 +16,9 @@ impl Render for FarLink<'_, T> { } } -pub struct Backlinks( - pub &'static [(&'static str, &'static str)], - pub &'static str, -); +pub struct Backlinks<'a>(pub &'a [(&'a str, &'a str)], pub &'a str); -impl Render for Backlinks { +impl Render for Backlinks<'_> { fn render(&self) -> Markup { html! { div { diff --git a/crates/service/service-webpage/src/components/mod.rs b/crates/service/service-webpage/src/components/mod.rs index bd279ec..bbc708c 100644 --- a/crates/service/service-webpage/src/components/mod.rs +++ b/crates/service/service-webpage/src/components/mod.rs @@ -1,4 +1,3 @@ -pub mod base; pub mod fa; pub mod mangle; pub mod md; diff --git a/crates/service/service-webpage/src/lib.rs b/crates/service/service-webpage/src/lib.rs index 7368703..2f7c8a7 100644 --- a/crates/service/service-webpage/src/lib.rs +++ b/crates/service/service-webpage/src/lib.rs @@ -1,8 +1,9 @@ use axum::Router; use libservice::ToService; -use utoipa::OpenApi; mod components; +mod page; +mod pages; mod routes; pub struct WebpageService {} @@ -20,11 +21,6 @@ impl ToService for WebpageService { Some(routes::router()) } - #[inline] - fn make_openapi(&self) -> utoipa::openapi::OpenApi { - routes::Api::openapi() - } - #[inline] fn service_name(&self) -> Option { Some("webpage".to_owned()) diff --git a/crates/service/service-webpage/src/page/mod.rs b/crates/service/service-webpage/src/page/mod.rs new file mode 100644 index 0000000..62990c4 --- /dev/null +++ b/crates/service/service-webpage/src/page/mod.rs @@ -0,0 +1,216 @@ +// +// MARK: metadata +// + +use axum::{ + Router, + extract::{ConnectInfo, Path, State}, + http::{HeaderValue, StatusCode, header}, + response::{IntoResponse, Response}, + routing::get, +}; +use chrono::{DateTime, Utc}; +use libservice::ServiceConnectInfo; +use lru::LruCache; +use maud::{Markup, Render, html}; +use parking_lot::Mutex; +use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration}; +use tracing::{debug, trace}; + +use crate::components::{md::Markdown, misc::Backlinks}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct PageMetadata { + pub title: String, + pub author: Option, + pub description: Option, + pub image: Option, +} + +impl Default for PageMetadata { + fn default() -> Self { + Self { + title: "Untitled page".into(), + author: None, + description: None, + image: None, + } + } +} + +impl Render for PageMetadata { + fn render(&self) -> Markup { + let empty = String::new(); + let title = &self.title; + let author = &self.author.as_ref().unwrap_or(&empty); + let description = &self.description.as_ref().unwrap_or(&empty); + let image = &self.image.as_ref().unwrap_or(&empty); + + html !( + meta property="og:site_name" content=(title) {} + meta name="title" content=(title) {} + meta property="og:title" content=(title) {} + meta property="twitter:title" content=(title) {} + + meta name="author" content=(author) {} + + meta name="description" content=(description) {} + meta property="og:description" content=(description) {} + meta property="twitter:description" content=(description) {} + + meta content=(image) property="og:image" {} + link rel="shortcut icon" href=(image) type="image/x-icon" {} + ) + } +} + +// +// MARK: page +// + +// Some HTML +pub struct Page { + pub meta: PageMetadata, + + /// How long this page's html may be cached. + /// + /// If `None`, this page is always rendered from scratch. + pub html_ttl: Option, + + /// A function that generates this page's html. + /// + /// This should return the contents of this page's tag, + /// or the contents of a wrapper element (defined in the page server struct). + /// + /// This closure must never return `` or ``. + pub generate_html: Box Markup>, +} + +impl Default for Page { + fn default() -> Self { + Page { + meta: Default::default(), + html_ttl: Some(Duration::from_secs(60 * 24 * 30)), + //css_ttl: Duration::from_secs(60 * 24 * 30), + //generate_css: None, + generate_html: Box::new(|_| html!()), + } + } +} + +impl Page { + pub fn generate_html(&self) -> Markup { + (self.generate_html)(self) + } + + pub fn from_markdown(meta: PageMetadata, md: impl Into) -> Self { + let md: String = md.into(); + + // TODO: define metadata and backlinks in markdown + Page { + meta, + generate_html: Box::new(move |page| { + html! { + (Backlinks(&[("/", "home")], &page.meta.title)) + (Markdown(&md)) + } + }), + + ..Default::default() + } + } +} + +// +// MARK: server +// + +pub struct PageServer { + /// Map of `{ route: page }` + pages: HashMap, + + /// Map of `{ route: (page data, expire time) }` + /// + /// We use an LruCache for bounded memory usage. + html_cache: Mutex)>>, + + /// Called whenever we need to render a page. + /// - this method should call `page.generate_html()`, + /// - wrap the result in ``, + /// - and add `` + /// ``` + render_page: Box Markup>, +} + +impl PageServer { + pub fn new(page_wrapper: Box Markup>) -> Self { + #[expect(clippy::unwrap_used)] + let cache_size = LruCache::new(NonZero::new(128).unwrap()); + + Self { + pages: HashMap::new(), + html_cache: Mutex::new(cache_size), + render_page: Box::new(page_wrapper), + } + } + + pub fn add_page(mut self, route: impl Into, page: Page) -> Self { + #[expect(clippy::expect_used)] + let route = route + .into() + .strip_prefix("/") + .expect("page route must start with /") + .to_owned(); + + self.pages.insert(route, page); + + self + } + + async fn handler( + Path(path): Path, + State(state): State>, + ConnectInfo(addr): ConnectInfo, + ) -> Response { + trace!("Serving {path} to {}", addr.addr); + + let page = match state.pages.get(&path) { + Some(x) => x, + + // TODO: 404 page + None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(), + }; + + let now = Utc::now(); + let headers = [( + header::CONTENT_TYPE, + HeaderValue::from_static("text/html; charset=utf-8"), + )]; + + if let Some((html, expires)) = state.html_cache.lock().get(&path) + && *expires > now + { + // TODO: no clone? + return (headers, html.clone()).into_response(); + }; + + debug!("Rendering {path}"); + let html = (state.render_page)(page).0; + + if let Some(ttl) = page.html_ttl { + state.html_cache.lock().put(path, (html.clone(), now + ttl)); + } + + return (headers, html.clone()).into_response(); + } + + pub fn into_router(self) -> Router<()> { + Router::new() + .route( + "/", + get(|state, conn| async { Self::handler(Path(String::new()), state, conn).await }), + ) + .route("/{*path}", get(Self::handler)) + .with_state(Arc::new(self)) + } +} diff --git a/crates/service/service-webpage/src/routes/betalupi.rs b/crates/service/service-webpage/src/pages/betalupi.rs similarity index 77% rename from crates/service/service-webpage/src/routes/betalupi.rs rename to crates/service/service-webpage/src/pages/betalupi.rs index 91530a7..decbf2c 100644 --- a/crates/service/service-webpage/src/routes/betalupi.rs +++ b/crates/service/service-webpage/src/pages/betalupi.rs @@ -1,34 +1,30 @@ use assetserver::Asset; -use maud::{Markup, html}; +use maud::html; use crate::{ - components::{ - base::{BasePage, PageMetadata}, - md::Markdown, - misc::Backlinks, - }, + components::{md::Markdown, misc::Backlinks}, + page::{Page, PageMetadata}, routes::assets::{Image_Betalupi, Image_Icon}, }; -pub async fn betalupi() -> Markup { - let meta = PageMetadata { - title: "What's a \"betalupi?\"".into(), - author: Some("Mark".into()), - description: None, - image: Some(Image_Icon::URL.into()), - }; +pub fn page() -> Page { + Page { + meta: PageMetadata { + title: "What's a \"betalupi?\"".into(), + author: Some("Mark".into()), + description: None, + image: Some(Image_Icon::URL.into()), + }, - html! { - (BasePage( - meta, - html!( + generate_html: Box::new(|_page| { + html! { (Backlinks(&[("/", "home")], "whats-a-betalupi")) - (Markdown(MD)) - img alt="betalupi map" class="image" src=(Image_Betalupi::URL) {} - ) - )) + } + }), + + ..Default::default() } } diff --git a/crates/service/service-webpage/src/routes/handouts.rs b/crates/service/service-webpage/src/pages/handouts.rs similarity index 95% rename from crates/service/service-webpage/src/routes/handouts.rs rename to crates/service/service-webpage/src/pages/handouts.rs index a4faa63..b75d8cd 100644 --- a/crates/service/service-webpage/src/routes/handouts.rs +++ b/crates/service/service-webpage/src/pages/handouts.rs @@ -1,32 +1,29 @@ use assetserver::Asset; -use maud::{Markup, html}; +use maud::html; use crate::{ - components::{ - base::{BasePage, PageMetadata}, - md::Markdown, - misc::Backlinks, - }, + components::{md::Markdown, misc::Backlinks}, + page::{Page, PageMetadata}, routes::assets::Image_Icon, }; -pub async fn handouts() -> Markup { - let meta = PageMetadata { - title: "Mark's Handouts".into(), - author: Some("Mark".into()), - description: None, - image: Some(Image_Icon::URL.into()), - }; +pub fn page() -> Page { + Page { + meta: PageMetadata { + title: "Mark's Handouts".into(), + author: Some("Mark".into()), + description: None, + image: Some(Image_Icon::URL.into()), + }, - html! { - (BasePage( - meta, - html!( + generate_html: Box::new(|_page| { + html! { (Backlinks(&[("/", "home")], "handouts")) - (Markdown(MD_A)) - ) - )) + } + }), + + ..Default::default() } } diff --git a/crates/service/service-webpage/src/routes/index.md b/crates/service/service-webpage/src/pages/index.md similarity index 100% rename from crates/service/service-webpage/src/routes/index.md rename to crates/service/service-webpage/src/pages/index.md diff --git a/crates/service/service-webpage/src/routes/index.rs b/crates/service/service-webpage/src/pages/index.rs similarity index 76% rename from crates/service/service-webpage/src/routes/index.rs rename to crates/service/service-webpage/src/pages/index.rs index d5f47d6..d2ef516 100644 --- a/crates/service/service-webpage/src/routes/index.rs +++ b/crates/service/service-webpage/src/pages/index.rs @@ -1,29 +1,28 @@ use assetserver::Asset; -use maud::{Markup, html}; +use maud::html; use crate::{ components::{ - base::{BasePage, PageMetadata}, fa::FAIcon, mangle::{MangledBetaEmail, MangledGoogleEmail}, md::Markdown, misc::FarLink, }, + page::{Page, PageMetadata}, routes::assets::{Image_Cover, Image_Icon}, }; -pub async fn index() -> Markup { - let meta = PageMetadata { - title: "Betalupi: About".into(), - author: Some("Mark".into()), - description: Some("Description".into()), - image: Some(Image_Icon::URL.into()), - }; +pub fn page() -> Page { + Page { + meta: PageMetadata { + title: "Betalupi: About".into(), + author: Some("Mark".into()), + description: Some("Description".into()), + image: Some(Image_Icon::URL.into()), + }, - html! { - (BasePage( - meta, - html!( + generate_html: Box::new(move |_page| { + html! { h2 id="about" { "About" } div { @@ -68,9 +67,9 @@ pub async fn index() -> Markup { br style="clear:both;" {} } - (Markdown(include_str!("index.md"))) - ) - )) + } + }), + ..Default::default() } } diff --git a/crates/service/service-webpage/src/routes/links.md b/crates/service/service-webpage/src/pages/links.md similarity index 100% rename from crates/service/service-webpage/src/routes/links.md rename to crates/service/service-webpage/src/pages/links.md diff --git a/crates/service/service-webpage/src/pages/links.rs b/crates/service/service-webpage/src/pages/links.rs new file mode 100644 index 0000000..5790c57 --- /dev/null +++ b/crates/service/service-webpage/src/pages/links.rs @@ -0,0 +1,35 @@ +use assetserver::Asset; +use maud::html; + +use crate::{ + components::{md::Markdown, misc::Backlinks}, + page::{Page, PageMetadata}, + routes::assets::Image_Icon, +}; + +pub fn page() -> Page { + Page { + meta: PageMetadata { + title: "Links".into(), + author: Some("Mark".into()), + description: None, + image: Some(Image_Icon::URL.into()), + }, + + generate_html: Box::new(|_page| { + html! { + (Backlinks(&[("/", "home")], "links")) + (Markdown(include_str!("links.md"))) + } + }), + + ..Default::default() + } +} + +/* +Dead links: + +https://www.commitstrip.com/en/ +http://www.3dprintmath.com/ +*/ diff --git a/crates/service/service-webpage/src/pages/mod.rs b/crates/service/service-webpage/src/pages/mod.rs new file mode 100644 index 0000000..e227dd0 --- /dev/null +++ b/crates/service/service-webpage/src/pages/mod.rs @@ -0,0 +1,4 @@ +pub mod betalupi; +pub mod handouts; +pub mod index; +pub mod links; diff --git a/crates/service/service-webpage/src/routes/links.rs b/crates/service/service-webpage/src/routes/links.rs deleted file mode 100644 index b22cdae..0000000 --- a/crates/service/service-webpage/src/routes/links.rs +++ /dev/null @@ -1,37 +0,0 @@ -use assetserver::Asset; -use maud::{Markup, html}; - -use crate::{ - components::{ - base::{BasePage, PageMetadata}, - md::Markdown, - misc::Backlinks, - }, - routes::assets::Image_Icon, -}; - -pub async fn links() -> Markup { - let meta = PageMetadata { - title: "Links".into(), - author: Some("Mark".into()), - description: None, - image: Some(Image_Icon::URL.into()), - }; - - html! { - (BasePage( - meta, - html!( - (Backlinks(&[("/", "home")], "links")) - (Markdown(include_str!("links.md"))) - ) - )) - } -} - -/* -Dead links: - -https://www.commitstrip.com/en/ -http://www.3dprintmath.com/ -*/ diff --git a/crates/service/service-webpage/src/routes/mod.rs b/crates/service/service-webpage/src/routes/mod.rs index 0250021..ca2cfb7 100644 --- a/crates/service/service-webpage/src/routes/mod.rs +++ b/crates/service/service-webpage/src/routes/mod.rs @@ -1,26 +1,62 @@ +use assetserver::Asset; use axum::Router; -use axum::routing::get; +use maud::{DOCTYPE, PreEscaped, html}; use tracing::info; -use utoipa::OpenApi; + +use crate::{components::misc::FarLink, page::PageServer, pages, routes::assets::Styles_Main}; pub mod assets; -mod betalupi; -mod handouts; -mod index; -mod links; - -#[derive(OpenApi)] -#[openapi(tags(), paths(), components(schemas()))] -pub(super) struct Api; pub(super) fn router() -> Router<()> { let (asset_prefix, asset_router) = assets::asset_router(); info!("Serving assets at {asset_prefix}"); - Router::new() - .route("/", get(index::index)) - .route("/whats-a-betalupi", get(betalupi::betalupi)) - .route("/links", get(links::links)) - .route("/handouts", get(handouts::handouts)) - .nest(asset_prefix, asset_router) + let server = PageServer::new(Box::new(|page| { + html! { + (DOCTYPE) + html { + head { + meta charset="UTF" {} + meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {} + meta content="text/html; charset=UTF-8" http-equiv="content-type" {} + meta property="og:type" content="website" {} + + link rel="stylesheet" href=(Styles_Main::URL) {} + + (&page.meta) + title { (PreEscaped(page.meta.title.clone())) } + } + + body { + div class="wrapper" { + main { ( page.generate_html() ) } + + footer { + hr class = "footline" {} + div class = "footContainer" { + p { + "This site was built by hand using " + (FarLink("https://rust-lang.org", "Rust")) + ", " + (FarLink("https://maud.lambda.xyz", "Maud")) + ", " + (FarLink("https://github.com/connorskees/grass", "Grass")) + ", and " + (FarLink("https://docs.rs/axum/latest/axum", "Axum")) + "." + } + } + } + } + } + } + } + })) + .add_page("/", pages::index::page()) + .add_page("/links", pages::links::page()) + .add_page("/whats-a-betalupi", pages::betalupi::page()) + .add_page("/handouts", pages::handouts::page()) + .into_router(); + + Router::new().merge(server).nest(asset_prefix, asset_router) }