page rewrite
Some checks failed
CI / Check typos (push) Failing after 9s
CI / Check links (push) Failing after 14s
CI / Clippy (push) Successful in 53s
CI / Build and test (push) Successful in 1m19s
CI / Build container (push) Has been skipped
CI / Deploy on waypoint (push) Has been skipped

This commit is contained in:
2025-11-15 23:01:37 -08:00
parent 04d98462dd
commit de6136fb31
22 changed files with 1462 additions and 729 deletions

View File

@@ -28,3 +28,4 @@ toml = { workspace = true }
serde = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true }
tower-http = { workspace = true }

View File

@@ -1,9 +1,8 @@
use lazy_static::lazy_static;
use markdown_it::generics::inline::full_link;
use markdown_it::{MarkdownIt, Node};
use maud::{Markup, PreEscaped, Render, html};
use page::RequestContext;
use page::page::{Page, PageMetadata};
use maud::{Markup, PreEscaped, Render};
use page::servable::PageMetadata;
use crate::components::md::emote::InlineEmote;
use crate::components::md::frontmatter::{TomlFrontMatter, YamlFrontMatter};
@@ -86,10 +85,6 @@ impl Markdown<'_> {
}
}
//
// MARK: helpers
//
/// Try to read page metadata from a markdown file's frontmatter.
/// - returns `none` if there is no frontmatter
/// - returns an error if we fail to parse frontmatter
@@ -101,33 +96,3 @@ pub fn meta_from_markdown(root_node: &Node) -> Result<Option<PageMetadata>, toml
.map(|x| toml::from_str::<PageMetadata>(&x.content))
.map_or(Ok(None), |v| v.map(Some))
}
pub fn backlinks(page: &Page, ctx: &RequestContext) -> Option<Markup> {
let mut last = None;
let mut backlinks = vec![("/", "home")];
if page.meta.backlinks.unwrap_or(false) {
let mut segments = ctx.route.split("/").skip(1).collect::<Vec<_>>();
last = segments.pop();
let mut end = 0;
for s in segments {
end += s.len();
backlinks.push((&ctx.route[0..=end], s));
end += 1; // trailing slash
}
}
last.map(|last| {
html! {
div {
@for (url, text) in backlinks {
a href=(url) style="padding-left:5pt;padding-right:5pt;" { (text) }
"/"
}
span style="color:var(--metaColor);padding-left:5pt;padding-right:5pt;" { (last) }
}
}
})
}

View File

@@ -6,18 +6,17 @@ use std::{
use chrono::{DateTime, TimeDelta, Utc};
use maud::{Markup, PreEscaped, html};
use page::{DeviceType, RequestContext, page::Page};
use page::{DeviceType, RenderContext, servable::Page};
use parking_lot::Mutex;
use reqwest::StatusCode;
use serde::Deserialize;
use tracing::{debug, warn};
use crate::{
components::{
md::{Markdown, backlinks, meta_from_markdown},
md::{Markdown, meta_from_markdown},
misc::FarLink,
},
pages::page_wrapper,
pages::{MAIN_TEMPLATE, backlinks, footer},
};
#[derive(Debug, Deserialize)]
@@ -114,11 +113,7 @@ async fn get_index() -> Result<Vec<HandoutEntry>, reqwest::Error> {
return Ok(res);
}
fn build_list_for_group(
handouts: &[HandoutEntry],
group: &str,
req_ctx: &RequestContext,
) -> Markup {
fn build_list_for_group(handouts: &[HandoutEntry], group: &str, req_ctx: &RenderContext) -> Markup {
let mobile = req_ctx.client_info.device_type == DeviceType::Mobile;
if mobile {
@@ -198,74 +193,79 @@ pub fn handouts() -> Page {
let html = PreEscaped(md.render());
Page {
meta,
html_ttl: Some(TimeDelta::seconds(300)),
immutable: false,
response_code: StatusCode::OK,
generate_html: Box::new(move |page, ctx| {
let html = html.clone(); // TODO: find a way to not clone here
MAIN_TEMPLATE
.derive(meta, move |page, ctx| {
let html = html.clone();
let index = index.clone();
Box::pin(async move {
let handouts = index.get().await;
let fallback = html! {
span style="color:var(--yellow)" {
"Could not load handouts, something broke."
}
" "
(
FarLink(
"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
"Try this direct link."
)
)
};
let warmups = match &*handouts {
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", ctx),
Err(error) => {
warn!("Could not load handout index: {error:?}");
fallback.clone()
}
};
let advanced = match &*handouts {
Ok(handouts) => build_list_for_group(handouts, "Advanced", ctx),
Err(_) => fallback,
};
let inner = html! {
@if let Some(backlinks) = backlinks(page, ctx) {
(backlinks)
}
(html)
(Markdown(concat!(
"## Warm-Ups",
"\n\n",
"Students never show up on time. Some come early, some come late. Warm-ups ",
"are my solution to this problem: we hand these out as students walk in, ",
"giving them something to do until we can start the lesson.",
)))
(warmups)
br {}
(Markdown(concat!(
"## Advanced",
"\n\n",
"The highest level of the ORMC, and the group I spend most of my time with. ",
"Students in ORMC Advanced are in high school, which means ",
"they're ~14-18 years old.",
)))
(advanced)
br {}
};
page_wrapper(&page.meta, inner, true).await
})
}),
}
render(html, index, page, ctx)
})
.html_ttl(Some(TimeDelta::seconds(300)))
}
fn render<'a>(
html: Markup,
index: Arc<CachedRequest<Result<Vec<HandoutEntry>, reqwest::Error>>>,
_page: &'a Page,
ctx: &'a RenderContext,
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>> {
Box::pin(async move {
let handouts = index.get().await;
let fallback = html! {
span style="color:var(--yellow)" {
"Could not load handouts, something broke."
}
" "
(
FarLink(
"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
"Try this direct link."
)
)
};
let warmups = match &*handouts {
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", ctx),
Err(error) => {
warn!("Could not load handout index: {error:?}");
fallback.clone()
}
};
let advanced = match &*handouts {
Ok(handouts) => build_list_for_group(handouts, "Advanced", ctx),
Err(_) => fallback,
};
html! {
div class="wrapper" style="margin-top:3ex;" {
@if let Some(backlinks) = backlinks(ctx) {
(backlinks)
}
(html)
(Markdown(concat!(
"## Warm-Ups",
"\n\n",
"Students never show up on time. Some come early, some come late. Warm-ups ",
"are my solution to this problem: we hand these out as students walk in, ",
"giving them something to do until we can start the lesson.",
)))
(warmups)
br {}
(Markdown(concat!(
"## Advanced",
"\n\n",
"The highest level of the ORMC, and the group I spend most of my time with. ",
"Students in ORMC Advanced are in high school, which means ",
"they're ~14-18 years old.",
)))
(advanced)
br {}
(footer())
}
}
})
}

View File

@@ -1,5 +1,9 @@
use maud::html;
use page::page::{Page, PageMetadata};
use maud::{Markup, html};
use page::{
RenderContext,
servable::{Page, PageMetadata},
};
use std::pin::Pin;
use crate::{
components::{
@@ -8,76 +12,78 @@ use crate::{
md::Markdown,
misc::FarLink,
},
pages::page_wrapper,
pages::{MAIN_TEMPLATE, footer},
};
pub fn index() -> Page {
Page {
meta: PageMetadata {
MAIN_TEMPLATE.derive(
PageMetadata {
title: "Betalupi: About".into(),
author: Some("Mark".into()),
description: None,
image: Some("/assets/img/icon.png".to_owned()),
backlinks: Some(false),
},
render,
)
}
generate_html: Box::new(move |page, _ctx| {
Box::pin(async {
let inner = html! {
h2 id="about" { "About" }
fn render<'a>(
_page: &'a Page,
_ctx: &'a RenderContext,
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>> {
Box::pin(async {
html! {
div class="wrapper" style="margin-top:3ex;" {
h2 id="about" { "About" }
div {
div {
img
class="img-placeholder"
src="/assets/img/cover-small.jpg?t=maxdim(20,20)"
data-large="/assets/img/cover-small.jpg"
style="image-rendering:pixelated;float:left;margin:10px;display:block;width:25%;"
{}
img
class="img-placeholder"
src="/assets/img/cover-small.jpg?t=maxdim(20,20)"
data-large="/assets/img/cover-small.jpg"
style="image-rendering:pixelated;float:left;margin:10px;display:block;width:25%;"
{}
div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" {
"Welcome, you've reached Mark's main page. Here you'll find"
" links to various projects I've worked on."
div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" {
"Welcome, you've reached Mark's main page. Here you'll find"
" links to various projects I've worked on."
ul {
li { (MangledBetaEmail {}) }
li { (MangledGoogleEmail {}) }
ul {
li { (MangledBetaEmail {}) }
li { (MangledGoogleEmail {}) }
li {
(
FarLink(
"https://github.com/rm-dr",
html!(
(FAIcon::Github)
"rm-dr"
)
li {
(
FarLink(
"https://github.com/rm-dr",
html!(
(FAIcon::Github)
"rm-dr"
)
)
}
)
}
li {
(
FarLink(
"https://git.betalupi.com",
html!(
(FAIcon::Git)
"git.betalupi.com"
)
li {
(
FarLink(
"https://git.betalupi.com",
html!(
(FAIcon::Git)
"git.betalupi.com"
)
)
}
)
}
}
br style="clear:both;" {}
}
br style="clear:both;" {}
}
(Markdown(include_str!("index.md")))
};
(Markdown(include_str!("index.md")))
page_wrapper(&page.meta, inner, true).await
})
}),
..Default::default()
}
(footer())
}
}
})
}

View File

@@ -1,12 +1,13 @@
use chrono::TimeDelta;
use maud::{DOCTYPE, Markup, PreEscaped, html};
use page::page::{Page, PageMetadata};
use reqwest::StatusCode;
use std::pin::Pin;
use maud::{Markup, PreEscaped, html};
use page::{
RenderContext,
servable::{Page, PageMetadata, PageTemplate},
};
use crate::components::{
fa::FAIcon,
md::{Markdown, backlinks, meta_from_markdown},
md::{Markdown, meta_from_markdown},
misc::FarLink,
};
@@ -67,120 +68,119 @@ fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> P
let html = PreEscaped(md.render());
Page {
meta,
immutable: true,
response_code: StatusCode::OK,
html_ttl: Some(TimeDelta::days(1)),
generate_html: Box::new(move |page, ctx| {
MAIN_TEMPLATE
.derive(meta, move |_page, ctx| {
let html = html.clone();
Box::pin(async move {
let inner = html! {
@if let Some(backlinks) = backlinks(page, ctx) {
(backlinks)
html! {
div class="wrapper" style="margin-top:3ex;" {
@if let Some(backlinks) = backlinks(ctx) {
(backlinks)
}
(html)
(footer())
}
(html)
};
page_wrapper(&page.meta, inner, true).await
}
})
}),
}
})
.html_ttl(Some(TimeDelta::days(1)))
.immutable(true)
}
//
// MARK: wrapper
// MARK: components
//
pub fn page_wrapper<'a>(
meta: &'a PageMetadata,
inner: Markup,
footer: bool,
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
Box::pin(async move {
const MAIN_TEMPLATE: PageTemplate = PageTemplate {
// Order matters, base htmx goes first
scripts_linked: &["/assets/htmx.js", "/assets/htmx-json.js"],
// TODO: use htmx for this
scripts_inline: &["
window.onload = function() {
var imgs = document.querySelectorAll('.img-placeholder');
imgs.forEach(img => {
img.style.border = 'none';
img.style.filter = 'blur(10px)';
img.style.transition = 'filter 0.3s';
var lg = new Image();
lg.src = img.dataset.large;
lg.onload = function () {
img.src = img.dataset.large;
img.style.filter = 'blur(0px)';
};
})
}
"],
styles_inline: &[],
styles_linked: &["/assets/css/main.css"],
extra_meta: &[(
"viewport",
"width=device-width,initial-scale=1,user-scalable=no",
)],
..PageTemplate::const_default()
};
pub fn backlinks(ctx: &RenderContext) -> Option<Markup> {
let mut backlinks = vec![("/", "home")];
let mut segments = ctx.route.split("/").skip(1).collect::<Vec<_>>();
let last = segments.pop();
let mut end = 0;
for s in segments {
end += s.len();
backlinks.push((&ctx.route[0..=end], s));
end += 1; // trailing slash
}
last.map(|last| {
html! {
(DOCTYPE)
html {
head {
meta charset="UTF8" {}
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=("/assets/css/main.css") {}
(&meta)
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 <img>!
script {
(PreEscaped("
window.onload = function() {
var imgs = document.querySelectorAll('.img-placeholder');
imgs.forEach(img => {
img.style.border = 'none';
img.style.filter = 'blur(10px)';
img.style.transition = 'filter 0.3s';
var lg = new Image();
lg.src = img.dataset.large;
lg.onload = function () {
img.src = img.dataset.large;
img.style.filter = 'blur(0px)';
};
})
}
"))
}
div {
@for (url, text) in backlinks {
a href=(url) style="padding-left:5pt;padding-right:5pt;" { (text) }
"/"
}
body {
main{
div class="wrapper" style=(
// for 404 page. Margin makes it scroll.
match footer {
true => "margin-top:3ex;",
false =>""
}
) {
(inner)
@if footer {
footer style="margin-top:10rem;" {
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"))
", and "
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
". "
(
FarLink(
"https://git.betalupi.com/Mark/webpage",
html!(
(FAIcon::Git)
"Source here!"
)
)
)
}
}
}
}
}
}}
span style="color:var(--metaColor);padding-left:5pt;padding-right:5pt;" { (last) }
}
}
})
}
pub fn footer() -> Markup {
html!(
footer style="margin-top:10rem;" {
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"))
", and "
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
". "
(
FarLink(
"https://git.betalupi.com/Mark/webpage",
html!(
(FAIcon::Git)
"Source here!"
)
)
)
}
}
}
)
}

View File

@@ -1,34 +1,29 @@
use maud::html;
use page::page::{Page, PageMetadata};
use page::servable::{Page, PageMetadata};
use reqwest::StatusCode;
use crate::pages::page_wrapper;
use crate::pages::MAIN_TEMPLATE;
pub fn notfound() -> Page {
Page {
meta: PageMetadata {
title: "Betalupi: About".into(),
author: None,
MAIN_TEMPLATE.derive(
PageMetadata {
title: "Page not found".into(),
author:None,
description: None,
image: Some("/assets/img/icon.png".to_owned()),
backlinks: Some(false),
},
response_code: StatusCode::NOT_FOUND,
generate_html: Box::new(move |page, _ctx| {
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"}
html! {
div class="wrapper" {
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()
}
},
).response_code(StatusCode::NOT_FOUND)
}

View File

@@ -1,27 +1,39 @@
use axum::Router;
use macro_sass::sass;
use page::{PageServer, asset::StaticAsset, redirect::Redirect};
use std::sync::Arc;
use page::{
ServableRoute,
servable::{Redirect, StaticAsset},
};
use toolbox::mime::MimeType;
use tower_http::compression::{CompressionLayer, DefaultPredicate};
use crate::pages;
pub(super) fn router() -> Router<()> {
build_server().into_router()
let compression: CompressionLayer = CompressionLayer::new()
.br(true)
.deflate(true)
.gzip(true)
.zstd(true)
.compress_when(DefaultPredicate::new());
build_server().into_router().layer(compression)
}
fn build_server() -> Arc<PageServer> {
let server = PageServer::new();
#[expect(clippy::unwrap_used)]
server
fn build_server() -> ServableRoute {
ServableRoute::new()
.with_404(pages::notfound())
.add_page("/", pages::index())
.add_page("/links", pages::links())
.add_page("/whats-a-betalupi", pages::betalupi())
.add_page("/handouts", pages::handouts())
.add_page("/htwah", Redirect::new("/handouts").unwrap())
.add_page("/htwah", {
#[expect(clippy::unwrap_used)]
Redirect::new("/handouts").unwrap()
})
.add_page("/htwah/typesetting", pages::htwah_typesetting())
.add_page("/assets/htmx.js", page::HTMX_2_0_8)
.add_page("/assets/htmx-json.js", page::EXT_JSON_1_19_12)
//
.add_page(
"/assets/css/main.css",
@@ -185,9 +197,7 @@ fn build_server() -> Arc<PageServer> {
bytes: include_bytes!("../../assets/htwah/spacing-b.pdf"),
mime: MimeType::Pdf,
},
);
server
)
}
#[test]
@@ -197,7 +207,6 @@ fn server_builds_without_panic() {
.build()
.unwrap()
.block_on(async {
// Needs tokio context
let _server = build_server().into_router();
let _server = build_server();
});
}