From d5067ff38110f3b1786da484d3337cf25a789735 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Tue, 11 Nov 2025 23:00:42 -0800 Subject: [PATCH] 404 --- crates/lib/page/src/server.rs | 78 +++++++++++-------- crates/service/service-webpage/css/main.scss | 16 ++-- .../service-webpage/src/pages/handouts.rs | 2 +- .../service-webpage/src/pages/index.rs | 4 +- .../service/service-webpage/src/pages/mod.rs | 49 ++++++++---- .../service-webpage/src/pages/notfound.rs | 32 ++++++++ .../service/service-webpage/src/routes/mod.rs | 1 + 7 files changed, 121 insertions(+), 61 deletions(-) create mode 100644 crates/service/service-webpage/src/pages/notfound.rs diff --git a/crates/lib/page/src/server.rs b/crates/lib/page/src/server.rs index 47c48c8..fbb3a51 100644 --- a/crates/lib/page/src/server.rs +++ b/crates/lib/page/src/server.rs @@ -52,6 +52,29 @@ pub trait Servable: Send + Sync { ) -> Pin + 'a + Send + Sync>>; } +pub struct Default404 {} +impl Servable for Default404 { + fn render<'a>( + &'a self, + _ctx: &'a RequestContext, + ) -> Pin + 'a + Send + Sync>> { + Box::pin(async { + return Rendered { + code: StatusCode::NOT_FOUND, + body: RenderedBody::String("page not found".into()), + ttl: Some(TimeDelta::days(30)), + immutable: true, + headers: HeaderMap::new(), + mime: Some(MimeType::Html), + }; + }) + } +} + +// +// MARK: server +// + pub struct PageServer { /// If true, expired pages will be rerendered before being sent to the user. /// If false, requests never trigger rerenders. We rely on the rerender task. @@ -61,7 +84,9 @@ pub struct PageServer { never_rerender_on_request: bool, /// Map of `{ route: page }` - pages: Arc>>>, + pages: Mutex>>, + + notfound: Mutex>, /// Map of `{ route: (page data, expire time) }` /// @@ -75,12 +100,19 @@ impl PageServer { let cache_size = NonZero::new(128).unwrap(); Arc::new(Self { - pages: Arc::new(Mutex::new(HashMap::new())), + pages: Mutex::new(HashMap::new()), page_cache: Mutex::new(LruCache::new(cache_size)), never_rerender_on_request: true, + notfound: Mutex::new(Arc::new(Default404 {})), }) } + /// Set this server's "not found" page + pub fn with_404(&self, page: S) -> &Self { + *self.notfound.lock() = Arc::new(page); + self + } + pub fn add_page(&self, route: impl Into, page: S) -> &Self { #[expect(clippy::expect_used)] let route = route @@ -102,23 +134,23 @@ impl PageServer { reason: &'static str, route: &str, ctx: RequestContext, - ) -> Option<(Rendered, Option>)> { + ) -> (Rendered, Option>) { let now = Utc::now(); let start = Instant::now(); + let page = match self.pages.lock().get(route) { Some(x) => x.clone(), - None => return None, + None => self.notfound.lock().clone(), }; trace!( message = "Rendering page", - route, + route = route.to_owned(), reason, lock_time_ms = start.elapsed().as_millis() ); let rendered = page.render(&ctx).await; - //let html = (self.render_page)(&page, &req_ctx).await.0; let mut expires = None; if let Some(ttl) = rendered.ttl { @@ -129,8 +161,13 @@ impl PageServer { } let elapsed = start.elapsed().as_millis(); - trace!(message = "Rendered page", route, reason, time_ms = elapsed); - return Some((rendered, expires)); + trace!( + message = "Rendered page", + route = route.to_owned(), + reason, + time_ms = elapsed + ); + return (rendered, expires); } async fn handler( @@ -206,30 +243,7 @@ impl PageServer { if html_expires.is_none() { cached = false; - html_expires = match state.render_page("request", &route, ctx).await { - Some(x) => Some(x.clone()), - None => { - trace!( - message = "Not found", - route, - addr = ?addr.addr, - user_agent = ua, - device_type = ?client_info.device_type - ); - - trace!( - message = "Served route", - route, - addr = ?addr.addr, - user_agent = ua, - device_type = ?client_info.device_type, - cached, - time_ns = start.elapsed().as_nanos() - ); - - return StatusCode::NOT_FOUND.into_response(); - } - }; + html_expires = Some(state.render_page("request", &route, ctx).await); } #[expect(clippy::unwrap_used)] diff --git a/crates/service/service-webpage/css/main.scss b/crates/service/service-webpage/css/main.scss index 9d922c1..c90c969 100644 --- a/crates/service/service-webpage/css/main.scss +++ b/crates/service/service-webpage/css/main.scss @@ -81,9 +81,11 @@ body { color: var(--fgColor); } -main { - margin-top: 2ex; - overflow-wrap: break-word; +div.wrapper { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: space-between; } hr.footline { @@ -92,18 +94,14 @@ hr.footline { hr { border: 1pt dashed; + + width: 100%; } iframe { max-width: 90%; } -.wrapper { - min-height: 100vh; - display: flex; - flex-direction: column; - justify-content: space-between; -} .footContainer { padding-top: 0; diff --git a/crates/service/service-webpage/src/pages/handouts.rs b/crates/service/service-webpage/src/pages/handouts.rs index ca4c735..0eb03c2 100644 --- a/crates/service/service-webpage/src/pages/handouts.rs +++ b/crates/service/service-webpage/src/pages/handouts.rs @@ -262,7 +262,7 @@ pub fn handouts() -> Page { br {} }; - page_wrapper(&page.meta, inner).await + page_wrapper(&page.meta, inner, true).await }) }), } diff --git a/crates/service/service-webpage/src/pages/index.rs b/crates/service/service-webpage/src/pages/index.rs index 6cf920d..9ad0ebc 100644 --- a/crates/service/service-webpage/src/pages/index.rs +++ b/crates/service/service-webpage/src/pages/index.rs @@ -16,7 +16,7 @@ pub fn index() -> Page { meta: PageMetadata { title: "Betalupi: About".into(), author: Some("Mark".into()), - description: Some("Description".into()), + description: None, image: Some("/assets/img/icon.png".to_owned()), backlinks: Some(false), }, @@ -74,7 +74,7 @@ pub fn index() -> Page { (Markdown(include_str!("index.md"))) }; - page_wrapper(&page.meta, inner).await + page_wrapper(&page.meta, inner, true).await }) }), diff --git a/crates/service/service-webpage/src/pages/mod.rs b/crates/service/service-webpage/src/pages/mod.rs index 8f83503..4eac447 100644 --- a/crates/service/service-webpage/src/pages/mod.rs +++ b/crates/service/service-webpage/src/pages/mod.rs @@ -10,9 +10,11 @@ use crate::components::{ mod handouts; mod index; +mod notfound; pub use handouts::handouts; pub use index::index; +pub use notfound::notfound; pub fn links() -> Page { /* @@ -80,7 +82,7 @@ fn page_from_markdown(md: impl Into, default_image: Option) -> P (html) }; - page_wrapper(&page.meta, inner).await + page_wrapper(&page.meta, inner, true).await }) }), } @@ -93,6 +95,7 @@ fn page_from_markdown(md: impl Into, default_image: Option) -> P pub fn page_wrapper<'a>( meta: &'a PageMetadata, inner: Markup, + footer: bool, ) -> Pin + 'a + Send + Sync>> { Box::pin(async move { html! { @@ -110,6 +113,9 @@ pub fn page_wrapper<'a>( title { (PreEscaped(meta.title.clone())) } + // Use a small blurred placeholder while full-size images load. + // Requires no other special scripts or css, just add some tags + // to your ! script { (PreEscaped(" window.onload = function() { @@ -133,27 +139,36 @@ pub fn page_wrapper<'a>( } body { - div class="wrapper" { - main { (inner) } + main{ + div class="wrapper" style=( + // for 404 page. Margin makes it scroll. + match footer { + true => "margin-top:3ex;", + false =>"" + } + ) { + (inner) - footer { - hr class = "footline" {} - div class = "footContainer" { - p { - "This site was built by hand with " - (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")) - "." + @if footer { + footer { + hr class = "footline" {} + div class = "footContainer" { + p { + "This site was built by hand with " + (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/pages/notfound.rs b/crates/service/service-webpage/src/pages/notfound.rs new file mode 100644 index 0000000..6e848f8 --- /dev/null +++ b/crates/service/service-webpage/src/pages/notfound.rs @@ -0,0 +1,32 @@ +use maud::html; +use page::page::{Page, PageMetadata}; + +use crate::pages::page_wrapper; + +pub fn notfound() -> Page { + Page { + meta: PageMetadata { + title: "Betalupi: About".into(), + author: None, + description: None, + image: Some("/assets/img/icon.png".to_owned()), + backlinks: Some(false), + }, + + generate_html: Box::new(move |page, _ctx| { + Box::pin(async { + let inner = html! { + div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh" { + p style="font-weight:bold;font-size:50pt;margin:0;" { "404" } + p style="font-size:13pt;margin:0;color:var(--grey);" { "(page not found)" } + a style="font-size:12pt;margin:10pt;padding:5px;" href="/" {"<- Back to site"} + } + }; + + page_wrapper(&page.meta, inner, false).await + }) + }), + + ..Default::default() + } +} diff --git a/crates/service/service-webpage/src/routes/mod.rs b/crates/service/service-webpage/src/routes/mod.rs index 26ba875..e568917 100644 --- a/crates/service/service-webpage/src/routes/mod.rs +++ b/crates/service/service-webpage/src/routes/mod.rs @@ -15,6 +15,7 @@ fn build_server() -> Arc { #[expect(clippy::unwrap_used)] server + .with_404(pages::notfound()) .add_page("/", pages::index()) .add_page("/links", pages::links()) .add_page("/whats-a-betalupi", pages::betalupi())