Generic servable
Some checks failed
CI / Check typos (push) Successful in 8s
CI / Check links (push) Failing after 7s
CI / Clippy (push) Successful in 51s
CI / Build and test (push) Successful in 1m13s
CI / Build container (push) Successful in 53s
CI / Deploy on waypoint (push) Successful in 48s
Some checks failed
CI / Check typos (push) Successful in 8s
CI / Check links (push) Failing after 7s
CI / Clippy (push) Successful in 51s
CI / Build and test (push) Successful in 1m13s
CI / Build container (push) Successful in 53s
CI / Deploy on waypoint (push) Successful in 48s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
mod page;
|
mod servable;
|
||||||
pub use page::*;
|
pub use servable::*;
|
||||||
|
|
||||||
mod requestcontext;
|
mod requestcontext;
|
||||||
pub use requestcontext::*;
|
pub use requestcontext::*;
|
||||||
|
|||||||
2
crates/lib/page/src/servable/mod.rs
Normal file
2
crates/lib/page/src/servable/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod page;
|
||||||
|
pub mod redirect;
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
use axum::http::{
|
||||||
|
HeaderMap, HeaderValue, StatusCode,
|
||||||
|
header::{self},
|
||||||
|
};
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use crate::RequestContext;
|
use crate::{Rendered, RequestContext, Servable};
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: metadata
|
// MARK: metadata
|
||||||
@@ -99,7 +103,31 @@ impl Default for Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub async fn generate_html(&self, req_info: &RequestContext) -> Markup {
|
pub async fn generate_html(&self, ctx: &RequestContext) -> Markup {
|
||||||
(self.generate_html)(self, req_info).await
|
(self.generate_html)(self, ctx).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Servable for Page {
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a RequestContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = crate::Rendered> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async {
|
||||||
|
let mut headers = HeaderMap::with_capacity(3);
|
||||||
|
let html = self.generate_html(ctx).await;
|
||||||
|
|
||||||
|
headers.append(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Rendered {
|
||||||
|
code: StatusCode::OK,
|
||||||
|
headers,
|
||||||
|
body: html.0.into_bytes(),
|
||||||
|
ttl: self.html_ttl,
|
||||||
|
};
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
39
crates/lib/page/src/servable/redirect.rs
Normal file
39
crates/lib/page/src/servable/redirect.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use axum::http::{
|
||||||
|
HeaderMap, HeaderValue, StatusCode,
|
||||||
|
header::{self, InvalidHeaderValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Rendered, RequestContext, Servable};
|
||||||
|
|
||||||
|
pub struct Redirect {
|
||||||
|
to: HeaderValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Redirect {
|
||||||
|
pub fn new(to: impl Into<String>) -> Result<Self, InvalidHeaderValue> {
|
||||||
|
Ok(Self {
|
||||||
|
to: HeaderValue::from_str(&to.into())?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Servable for Redirect {
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
_ctx: &'a RequestContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async {
|
||||||
|
let mut headers = HeaderMap::with_capacity(1);
|
||||||
|
headers.append(header::LOCATION, self.to.clone());
|
||||||
|
|
||||||
|
return Rendered {
|
||||||
|
code: StatusCode::PERMANENT_REDIRECT,
|
||||||
|
headers,
|
||||||
|
body: Vec::new(),
|
||||||
|
ttl: None,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,31 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use libservice::ServiceConnectInfo;
|
use libservice::ServiceConnectInfo;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use maud::Markup;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
|
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
|
||||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::{ClientInfo, RequestContext, page::Page};
|
use crate::{ClientInfo, RequestContext};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Rendered {
|
||||||
|
pub code: StatusCode,
|
||||||
|
pub headers: HeaderMap,
|
||||||
|
pub body: Vec<u8>,
|
||||||
|
|
||||||
|
pub ttl: Option<TimeDelta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Servable: Send + Sync {
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a RequestContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered> + 'a + Send + Sync>>;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PageServer {
|
pub struct PageServer {
|
||||||
/// If true, expired pages will be rerendered before being sent to the user.
|
/// If true, expired pages will be rerendered before being sent to the user.
|
||||||
@@ -25,56 +40,32 @@ pub struct PageServer {
|
|||||||
never_rerender_on_request: bool,
|
never_rerender_on_request: bool,
|
||||||
|
|
||||||
/// Map of `{ route: page }`
|
/// Map of `{ route: page }`
|
||||||
pages: Arc<Mutex<HashMap<String, Arc<Page>>>>,
|
pages: Arc<Mutex<HashMap<String, Arc<dyn Servable>>>>,
|
||||||
|
|
||||||
/// Map of `{ route: (page data, expire time) }`
|
/// Map of `{ route: (page data, expire time) }`
|
||||||
///
|
///
|
||||||
/// We use an LruCache for bounded memory usage.
|
/// We use an LruCache for bounded memory usage.
|
||||||
html_cache: Mutex<LruCache<(String, RequestContext), (String, DateTime<Utc>)>>,
|
page_cache: Mutex<LruCache<RequestContext, (Rendered, DateTime<Utc>)>>,
|
||||||
|
|
||||||
/// Called whenever we need to render a page.
|
|
||||||
/// - this method should call `page.generate_html()`,
|
|
||||||
/// - wrap the result in `<html><body>`,
|
|
||||||
/// - and add `<head>`
|
|
||||||
/// ```
|
|
||||||
render_page: Box<
|
|
||||||
dyn Send
|
|
||||||
+ Sync
|
|
||||||
+ for<'a> Fn(
|
|
||||||
&'a Page,
|
|
||||||
&'a RequestContext,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
|
||||||
>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageServer {
|
impl PageServer {
|
||||||
pub fn new(
|
pub fn new() -> Arc<Self> {
|
||||||
render_page: Box<
|
|
||||||
dyn Send
|
|
||||||
+ Sync
|
|
||||||
+ for<'a> Fn(
|
|
||||||
&'a Page,
|
|
||||||
&'a RequestContext,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
|
||||||
>,
|
|
||||||
) -> Arc<Self> {
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
let cache_size = NonZero::new(128).unwrap();
|
let cache_size = NonZero::new(128).unwrap();
|
||||||
|
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
pages: Arc::new(Mutex::new(HashMap::new())),
|
pages: Arc::new(Mutex::new(HashMap::new())),
|
||||||
html_cache: Mutex::new(LruCache::new(cache_size)),
|
page_cache: Mutex::new(LruCache::new(cache_size)),
|
||||||
render_page,
|
|
||||||
never_rerender_on_request: true,
|
never_rerender_on_request: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_page(&self, route: impl Into<String>, page: Page) -> &Self {
|
pub fn add_page<S: Servable + 'static>(&self, route: impl Into<String>, page: S) -> &Self {
|
||||||
#[expect(clippy::expect_used)]
|
#[expect(clippy::expect_used)]
|
||||||
let route = route
|
let route = route
|
||||||
.into()
|
.into()
|
||||||
.strip_prefix("/")
|
.strip_prefix("/")
|
||||||
.expect("page route must start with /")
|
.expect("route must start with /")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
self.pages.lock().insert(route, Arc::new(page));
|
self.pages.lock().insert(route, Arc::new(page));
|
||||||
@@ -89,8 +80,8 @@ impl PageServer {
|
|||||||
&self,
|
&self,
|
||||||
reason: &'static str,
|
reason: &'static str,
|
||||||
route: &str,
|
route: &str,
|
||||||
req_ctx: RequestContext,
|
ctx: RequestContext,
|
||||||
) -> Option<(String, Option<DateTime<Utc>>)> {
|
) -> Option<(Rendered, Option<DateTime<Utc>>)> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let page = match self.pages.lock().get(route) {
|
let page = match self.pages.lock().get(route) {
|
||||||
@@ -105,19 +96,20 @@ impl PageServer {
|
|||||||
lock_time_ms = start.elapsed().as_millis()
|
lock_time_ms = start.elapsed().as_millis()
|
||||||
);
|
);
|
||||||
|
|
||||||
let html = (self.render_page)(&page, &req_ctx).await.0;
|
let rendered = page.render(&ctx).await;
|
||||||
|
//let html = (self.render_page)(&page, &req_ctx).await.0;
|
||||||
|
|
||||||
let mut expires = None;
|
let mut expires = None;
|
||||||
if let Some(ttl) = page.html_ttl {
|
if let Some(ttl) = rendered.ttl {
|
||||||
expires = Some(now + ttl);
|
expires = Some(now + ttl);
|
||||||
self.html_cache
|
self.page_cache
|
||||||
.lock()
|
.lock()
|
||||||
.put((route.to_owned(), req_ctx), (html.clone(), now + ttl));
|
.put(ctx, (rendered.clone(), now + ttl));
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = start.elapsed().as_millis();
|
let elapsed = start.elapsed().as_millis();
|
||||||
trace!(message = "Rendered page", route, reason, time_ms = elapsed);
|
trace!(message = "Rendered page", route, reason, time_ms = elapsed);
|
||||||
return Some((html, expires));
|
return Some((rendered, expires));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler(
|
async fn handler(
|
||||||
@@ -126,6 +118,7 @@ impl PageServer {
|
|||||||
ConnectInfo(addr): ConnectInfo<ServiceConnectInfo>,
|
ConnectInfo(addr): ConnectInfo<ServiceConnectInfo>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
let start = Instant::now();
|
||||||
let client_info = ClientInfo::from_headers(&headers);
|
let client_info = ClientInfo::from_headers(&headers);
|
||||||
let ua = headers
|
let ua = headers
|
||||||
.get("user-agent")
|
.get("user-agent")
|
||||||
@@ -172,24 +165,23 @@ impl PageServer {
|
|||||||
return (StatusCode::PERMANENT_REDIRECT, headers).into_response();
|
return (StatusCode::PERMANENT_REDIRECT, headers).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let req_ctx = RequestContext {
|
let ctx = RequestContext {
|
||||||
client_info,
|
client_info,
|
||||||
route: format!("/{route}"),
|
route: format!("/{route}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cache_key = (route.clone(), req_ctx.clone());
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let mut html_expires = None;
|
let mut html_expires = None;
|
||||||
|
|
||||||
// Get from cache, if available
|
// Get from cache, if available
|
||||||
if let Some((html, expires)) = state.html_cache.lock().get(&cache_key)
|
if let Some((html, expires)) = state.page_cache.lock().get(&ctx)
|
||||||
&& (*expires > now || state.never_rerender_on_request)
|
&& (*expires > now || state.never_rerender_on_request)
|
||||||
{
|
{
|
||||||
html_expires = Some((html.clone(), Some(*expires)));
|
html_expires = Some((html.clone(), Some(*expires)));
|
||||||
};
|
};
|
||||||
|
|
||||||
if html_expires.is_none() {
|
if html_expires.is_none() {
|
||||||
html_expires = match state.render_page("request", &route, req_ctx).await {
|
html_expires = match state.render_page("request", &route, ctx).await {
|
||||||
Some(x) => Some(x.clone()),
|
Some(x) => Some(x.clone()),
|
||||||
None => {
|
None => {
|
||||||
trace!(
|
trace!(
|
||||||
@@ -200,19 +192,22 @@ impl PageServer {
|
|||||||
device_type = ?client_info.device_type
|
device_type = ?client_info.device_type
|
||||||
);
|
);
|
||||||
|
|
||||||
return (StatusCode::NOT_FOUND, "page doesn't exist").into_response();
|
trace!(
|
||||||
|
message = "Served route",
|
||||||
|
route,
|
||||||
|
addr = ?addr.addr,
|
||||||
|
user_agent = ua,
|
||||||
|
device_type = ?client_info.device_type,
|
||||||
|
time_ms = start.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
|
||||||
|
return StatusCode::NOT_FOUND.into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
let (html, expires) = html_expires.unwrap();
|
let (mut html, expires) = html_expires.unwrap();
|
||||||
|
|
||||||
let mut headers = HeaderMap::with_capacity(3);
|
|
||||||
headers.append(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let max_age = match expires {
|
let max_age = match expires {
|
||||||
Some(expires) => (expires - now).num_seconds().max(1),
|
Some(expires) => (expires - now).num_seconds().max(1),
|
||||||
@@ -220,15 +215,25 @@ impl PageServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
headers.append(
|
html.headers.insert(
|
||||||
header::CACHE_CONTROL,
|
header::CACHE_CONTROL,
|
||||||
// immutable; public/private
|
// immutable; public/private
|
||||||
HeaderValue::from_str(&format!("immutable, public, max-age={}", max_age)).unwrap(),
|
HeaderValue::from_str(&format!("immutable, public, max-age={}", max_age)).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
headers.append("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
|
html.headers
|
||||||
|
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
|
||||||
|
|
||||||
return (headers, html).into_response();
|
trace!(
|
||||||
|
message = "Served route",
|
||||||
|
route,
|
||||||
|
addr = ?addr.addr,
|
||||||
|
user_agent = ua,
|
||||||
|
device_type = ?client_info.device_type,
|
||||||
|
time_ns = start.elapsed().as_nanos()
|
||||||
|
);
|
||||||
|
|
||||||
|
return (html.code, html.headers, html.body).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_router(self: Arc<Self>) -> Router<()> {
|
pub fn into_router(self: Arc<Self>) -> Router<()> {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use lazy_static::lazy_static;
|
|||||||
use markdown_it::generics::inline::full_link;
|
use markdown_it::generics::inline::full_link;
|
||||||
use markdown_it::{MarkdownIt, Node};
|
use markdown_it::{MarkdownIt, Node};
|
||||||
use maud::{Markup, PreEscaped, Render, html};
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
use page::{Page, PageMetadata, RequestContext};
|
use page::RequestContext;
|
||||||
|
use page::page::{Page, PageMetadata};
|
||||||
|
|
||||||
use crate::components::md::emote::InlineEmote;
|
use crate::components::md::emote::InlineEmote;
|
||||||
use crate::components::md::frontmatter::{TomlFrontMatter, YamlFrontMatter};
|
use crate::components::md::frontmatter::{TomlFrontMatter, YamlFrontMatter};
|
||||||
@@ -101,43 +102,6 @@ pub fn meta_from_markdown(root_node: &Node) -> Result<Option<PageMetadata>, toml
|
|||||||
.map_or(Ok(None), |v| v.map(Some))
|
.map_or(Ok(None), |v| v.map(Some))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> Page {
|
|
||||||
let md: String = md.into();
|
|
||||||
let md = Markdown::parse(&md);
|
|
||||||
|
|
||||||
let mut meta = meta_from_markdown(&md)
|
|
||||||
.unwrap_or(Some(PageMetadata {
|
|
||||||
title: "Invalid frontmatter!".into(),
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if meta.image.is_none() {
|
|
||||||
meta.image = default_image
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = PreEscaped(md.render());
|
|
||||||
|
|
||||||
Page {
|
|
||||||
meta,
|
|
||||||
generate_html: Box::new(move |page, ctx| {
|
|
||||||
let html = html.clone();
|
|
||||||
|
|
||||||
Box::pin(async move {
|
|
||||||
html! {
|
|
||||||
@if let Some(backlinks) = backlinks(page, ctx) {
|
|
||||||
(backlinks)
|
|
||||||
}
|
|
||||||
|
|
||||||
(html)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backlinks(page: &Page, ctx: &RequestContext) -> Option<Markup> {
|
pub fn backlinks(page: &Page, ctx: &RequestContext) -> Option<Markup> {
|
||||||
let mut last = None;
|
let mut last = None;
|
||||||
let mut backlinks = vec![("/", "home")];
|
let mut backlinks = vec![("/", "home")];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, PreEscaped, html};
|
||||||
use page::{DeviceType, Page, RequestContext};
|
use page::{DeviceType, RequestContext, page::Page};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
md::{Markdown, backlinks, meta_from_markdown},
|
md::{Markdown, backlinks, meta_from_markdown},
|
||||||
misc::FarLink,
|
misc::FarLink,
|
||||||
},
|
},
|
||||||
|
pages::page_wrapper,
|
||||||
routes::assets::Image_Icon,
|
routes::assets::Image_Icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ pub fn handouts() -> Page {
|
|||||||
Err(_) => fallback,
|
Err(_) => fallback,
|
||||||
};
|
};
|
||||||
|
|
||||||
html! {
|
let inner = html! {
|
||||||
@if let Some(backlinks) = backlinks(page, ctx) {
|
@if let Some(backlinks) = backlinks(page, ctx) {
|
||||||
(backlinks)
|
(backlinks)
|
||||||
}
|
}
|
||||||
@@ -260,7 +261,9 @@ pub fn handouts() -> Page {
|
|||||||
)))
|
)))
|
||||||
(advanced)
|
(advanced)
|
||||||
br {}
|
br {}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
page_wrapper(&page.meta, inner).await
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use page::{Page, PageMetadata};
|
use page::page::{Page, PageMetadata};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
md::Markdown,
|
md::Markdown,
|
||||||
misc::FarLink,
|
misc::FarLink,
|
||||||
},
|
},
|
||||||
|
pages::page_wrapper,
|
||||||
routes::assets::{Image_Cover, Image_Icon},
|
routes::assets::{Image_Cover, Image_Icon},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,9 +23,9 @@ pub fn index() -> Page {
|
|||||||
backlinks: Some(false),
|
backlinks: Some(false),
|
||||||
},
|
},
|
||||||
|
|
||||||
generate_html: Box::new(move |_page, _| {
|
generate_html: Box::new(move |page, _ctx| {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
html! {
|
let inner = html! {
|
||||||
h2 id="about" { "About" }
|
h2 id="about" { "About" }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -70,9 +71,12 @@ pub fn index() -> Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(Markdown(include_str!("index.md")))
|
(Markdown(include_str!("index.md")))
|
||||||
}
|
};
|
||||||
|
|
||||||
|
page_wrapper(&page.meta, inner).await
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use page::Page;
|
use chrono::TimeDelta;
|
||||||
|
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
|
use page::page::{Page, PageMetadata};
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
use crate::{components::md::page_from_markdown, routes::assets::Image_Icon};
|
use crate::{
|
||||||
|
components::{
|
||||||
|
md::{Markdown, backlinks, meta_from_markdown},
|
||||||
|
misc::FarLink,
|
||||||
|
},
|
||||||
|
routes::assets::{Image_Icon, Styles_Main},
|
||||||
|
};
|
||||||
|
|
||||||
mod handouts;
|
mod handouts;
|
||||||
mod index;
|
mod index;
|
||||||
@@ -33,3 +42,97 @@ pub fn htwah_typesetting() -> Page {
|
|||||||
Some(Image_Icon::URL.to_owned()),
|
Some(Image_Icon::URL.to_owned()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: md
|
||||||
|
//
|
||||||
|
|
||||||
|
fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> Page {
|
||||||
|
let md: String = md.into();
|
||||||
|
let md = Markdown::parse(&md);
|
||||||
|
|
||||||
|
let mut meta = meta_from_markdown(&md)
|
||||||
|
.unwrap_or(Some(PageMetadata {
|
||||||
|
title: "Invalid frontmatter!".into(),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if meta.image.is_none() {
|
||||||
|
meta.image = default_image
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = PreEscaped(md.render());
|
||||||
|
|
||||||
|
Page {
|
||||||
|
meta,
|
||||||
|
|
||||||
|
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)),
|
||||||
|
generate_html: Box::new(move |page, ctx| {
|
||||||
|
let html = html.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let inner = html! {
|
||||||
|
@if let Some(backlinks) = backlinks(page, ctx) {
|
||||||
|
(backlinks)
|
||||||
|
}
|
||||||
|
|
||||||
|
(html)
|
||||||
|
};
|
||||||
|
|
||||||
|
page_wrapper(&page.meta, inner).await
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: wrapper
|
||||||
|
//
|
||||||
|
|
||||||
|
pub fn page_wrapper<'a>(
|
||||||
|
meta: &'a PageMetadata,
|
||||||
|
inner: Markup,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
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 { (inner) }
|
||||||
|
|
||||||
|
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"))
|
||||||
|
"."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
use std::{pin::Pin, sync::Arc};
|
|
||||||
|
|
||||||
use assetserver::Asset;
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
use page::{PageServer, redirect::Redirect};
|
||||||
use page::{Page, PageServer, RequestContext};
|
use std::sync::Arc;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{components::misc::FarLink, pages, routes::assets::Styles_Main};
|
use crate::pages;
|
||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
||||||
@@ -20,63 +17,20 @@ pub(super) fn router() -> Router<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_server() -> Arc<PageServer> {
|
fn build_server() -> Arc<PageServer> {
|
||||||
let server = PageServer::new(Box::new(page_wrapper));
|
let server = PageServer::new();
|
||||||
|
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
server
|
server
|
||||||
.add_page("/", pages::index())
|
.add_page("/", pages::index())
|
||||||
.add_page("/links", pages::links())
|
.add_page("/links", pages::links())
|
||||||
.add_page("/whats-a-betalupi", pages::betalupi())
|
.add_page("/whats-a-betalupi", pages::betalupi())
|
||||||
.add_page("/handouts", pages::handouts())
|
.add_page("/handouts", pages::handouts())
|
||||||
|
.add_page("/htwah", Redirect::new("/handouts").unwrap())
|
||||||
.add_page("/htwah/typesetting", pages::htwah_typesetting());
|
.add_page("/htwah/typesetting", pages::htwah_typesetting());
|
||||||
|
|
||||||
server
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
fn page_wrapper<'a>(
|
|
||||||
page: &'a Page,
|
|
||||||
req_ctx: &'a RequestContext,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
|
|
||||||
Box::pin(async move {
|
|
||||||
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(req_ctx).await ) }
|
|
||||||
|
|
||||||
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"))
|
|
||||||
"."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn server_builds_without_panic() {
|
fn server_builds_without_panic() {
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
|||||||
Reference in New Issue
Block a user