Compare commits

..

1 Commits

Author SHA1 Message Date
2603c835c6 Redirect trailing slashes
All checks were successful
CI / Check typos (push) Successful in 10s
CI / Check links (push) Successful in 1m6s
CI / Clippy (push) Successful in 1m6s
CI / Build and test (push) Successful in 1m4s
CI / Build container (push) Successful in 45s
CI / Deploy on waypoint (push) Successful in 46s
2025-11-06 08:00:23 -08:00
12 changed files with 46 additions and 71 deletions

View File

@@ -15,7 +15,7 @@ pub struct PageMetadata {
pub author: Option<String>, pub author: Option<String>,
pub description: Option<String>, pub description: Option<String>,
pub image: Option<String>, pub image: Option<String>,
pub backlinks: Option<bool>, pub slug: Option<String>,
} }
impl Default for PageMetadata { impl Default for PageMetadata {
@@ -25,7 +25,7 @@ impl Default for PageMetadata {
author: None, author: None,
description: None, description: None,
image: None, image: None,
backlinks: None, slug: None,
} }
} }
} }

View File

@@ -3,7 +3,6 @@ use axum::http::HeaderMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RequestContext { pub struct RequestContext {
pub client_info: ClientInfo, pub client_info: ClientInfo,
pub route: String,
} }
// //

View File

@@ -131,12 +131,8 @@ impl PageServer {
.unwrap_or(""); .unwrap_or("");
// Normalize url with redirect // Normalize url with redirect
if route.ends_with('/') || route.contains("//") || route.starts_with('/') { if route.ends_with('/') {
let mut new_route = route.clone(); let new_route = route.trim_end_matches('/');
while new_route.contains("//") {
new_route = new_route.replace("//", "/");
}
let new_route = new_route.trim_matches('/');
trace!( trace!(
message = "Redirecting route", message = "Redirecting route",
@@ -148,16 +144,10 @@ impl PageServer {
); );
let mut headers = HeaderMap::with_capacity(2); let mut headers = HeaderMap::with_capacity(2);
headers.append(
let new_route = match HeaderValue::from_str(&format!("/{new_route}")) { header::LOCATION,
Ok(x) => x, HeaderValue::from_str(&format!("/{new_route}")).unwrap(),
Err(_) => { );
// Be extra careful, this is user-provided data
return StatusCode::BAD_REQUEST.into_response();
}
};
headers.append(header::LOCATION, new_route);
headers.append("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile")); headers.append("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
return (StatusCode::PERMANENT_REDIRECT, headers).into_response(); return (StatusCode::PERMANENT_REDIRECT, headers).into_response();
} }
@@ -170,10 +160,7 @@ impl PageServer {
device_type = ?client_info.device_type device_type = ?client_info.device_type
); );
let req_ctx = RequestContext { let req_ctx = RequestContext { client_info };
client_info,
route: format!("/{route}"),
};
let cache_key = (route.clone(), req_ctx.clone()); let cache_key = (route.clone(), req_ctx.clone());
let now = Utc::now(); let now = Utc::now();

View File

@@ -5,11 +5,12 @@ use markdown_it::parser::core::Root;
use markdown_it::parser::inline::{InlineRule, InlineState}; use markdown_it::parser::inline::{InlineRule, InlineState};
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
use maud::{Markup, PreEscaped, Render, html}; use maud::{Markup, PreEscaped, Render, html};
use page::{Page, PageMetadata, RequestContext}; use page::{Page, PageMetadata};
use std::str::FromStr; use std::str::FromStr;
use crate::components::fa::FAIcon; use crate::components::fa::FAIcon;
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail}; use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};
use crate::components::misc::Backlinks;
lazy_static! { lazy_static! {
static ref MdParser: MarkdownIt = { static ref MdParser: MarkdownIt = {
@@ -114,13 +115,12 @@ pub fn page_from_markdown(md: impl Into<String>, default_image: Option<String>)
Page { Page {
meta, meta,
generate_html: Box::new(move |page, ctx| { generate_html: Box::new(move |page, _| {
let html = html.clone(); let html = html.clone();
Box::pin(async move { Box::pin(async move {
html! { html! {
@if let Some(backlinks) = backlinks(page, ctx) { @if let Some(slug) = &page.meta.slug {
(backlinks) (Backlinks(&[("/", "home")], slug))
} }
(html) (html)
@@ -132,36 +132,6 @@ pub fn page_from_markdown(md: impl Into<String>, default_image: Option<String>)
} }
} }
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) }
}
}
})
}
// //
// MARK: extensions // MARK: extensions
// //

View File

@@ -15,3 +15,20 @@ impl<T: Render> Render for FarLink<'_, T> {
) )
} }
} }
pub struct Backlinks<'a>(pub &'a [(&'a str, &'a str)], pub &'a str);
impl Render for Backlinks<'_> {
fn render(&self) -> Markup {
html! {
div {
@for (url, text) in self.0 {
a href=(url) style="padding-left:5pt;padding-right:5pt;" { (text) }
"/"
}
span style="color:var(--metaColor);padding-left:5pt;padding-right:5pt;" { (self.1) }
}
}
}
}

View File

@@ -1,7 +1,7 @@
+++ +++
title = "What's a \"betalupi?\"" title = "What's a \"betalupi?\""
author = "Mark" author = "Mark"
backlinks = true slug = "whats-a-betalupi"
+++ +++
[es]: https://github.com/endless-sky/endless-sky [es]: https://github.com/endless-sky/endless-sky

View File

@@ -1,7 +1,7 @@
+++ +++
title = "Mark's Handouts" title = "Mark's Handouts"
author = "Mark" author = "Mark"
backlinks = true slug = "handouts"
+++ +++
# Mark's Handouts # Mark's Handouts

View File

@@ -14,8 +14,8 @@ use tracing::{debug, warn};
use crate::{ use crate::{
components::{ components::{
md::{Markdown, backlinks, meta_from_markdown}, md::{Markdown, meta_from_markdown},
misc::FarLink, misc::{Backlinks, FarLink},
}, },
routes::assets::Image_Icon, routes::assets::Image_Icon,
}; };
@@ -202,7 +202,7 @@ pub fn handouts() -> Page {
meta, meta,
html_ttl: Some(TimeDelta::seconds(300)), html_ttl: Some(TimeDelta::seconds(300)),
generate_html: Box::new(move |page, ctx| { generate_html: Box::new(move |page, req_ctx| {
let html = html.clone(); // TODO: find a way to not clone here let html = html.clone(); // TODO: find a way to not clone here
let index = index.clone(); let index = index.clone();
Box::pin(async move { Box::pin(async move {
@@ -222,7 +222,7 @@ pub fn handouts() -> Page {
}; };
let warmups = match &*handouts { let warmups = match &*handouts {
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", ctx), Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", req_ctx),
Err(error) => { Err(error) => {
warn!("Could not load handout index: {error:?}"); warn!("Could not load handout index: {error:?}");
fallback.clone() fallback.clone()
@@ -230,13 +230,13 @@ pub fn handouts() -> Page {
}; };
let advanced = match &*handouts { let advanced = match &*handouts {
Ok(handouts) => build_list_for_group(handouts, "Advanced", ctx), Ok(handouts) => build_list_for_group(handouts, "Advanced", req_ctx),
Err(_) => fallback, Err(_) => fallback,
}; };
html! { html! {
@if let Some(backlinks) = backlinks(page, ctx) { @if let Some(slug) = &page.meta.slug {
(backlinks) (Backlinks(&[("/", "home")], slug))
} }
(html) (html)

View File

@@ -1,7 +1,9 @@
+++ +++
title = "HtWaH: Typesetting" title = "HtWaH: Typesetting"
author = "Mark" author = "Mark"
backlinks = true
# TODO: many slugs, htwah/typesetting
slug = "handouts"
+++ +++
## Table of Contents ## Table of Contents

View File

@@ -19,7 +19,7 @@ pub fn index() -> Page {
author: Some("Mark".into()), author: Some("Mark".into()),
description: Some("Description".into()), description: Some("Description".into()),
image: Some(Image_Icon::URL.into()), image: Some(Image_Icon::URL.into()),
backlinks: Some(false), slug: None,
}, },
generate_html: Box::new(move |_page, _| { generate_html: Box::new(move |_page, _| {

View File

@@ -1,7 +1,7 @@
+++ +++
title = "Links" title = "Links"
author = "Mark" author = "Mark"
backlinks = true slug = "links"
+++ +++

View File

@@ -26,7 +26,7 @@ fn build_server() -> Arc<PageServer> {
.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/typesetting", pages::htwah_typesetting()); .add_page("/htwah", pages::htwah_typesetting());
server server
} }