diff --git a/Cargo.lock b/Cargo.lock index ac27c3e..01a445a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2307,7 +2307,6 @@ dependencies = [ "chrono", "color-eyre", "glob", - "hyper", "lazy_static", "opentelemetry", "prometheus", diff --git a/Cargo.toml b/Cargo.toml index fae2cb1..4633d3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = { version = "0.6.12", features = ["http2"] } +axum = { version = "0.6.12", features = ["http2", "original-uri"] } cached = "0.44.0" chrono = { version = "0.4.24", features = ["serde"] } color-eyre = "0.6.1" glob = "0.3.0" -hyper = { version = "0.14.19", features = ["full"] } +# hyper = { version = "0.14.19", features = ["full"] } lazy_static = "1.4.0" opentelemetry = { version = "0.19.0", features = ["metrics"] } prometheus = { version = "0.13.3", features = ["process"] } diff --git a/src/feed.rs b/src/feed.rs index a4f7bbb..46d50a0 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -24,8 +24,8 @@ pub fn render_atom_feed(state: &AppState) -> Result { let updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten(); let feed = FeedContext { - feed_url: &format!("{}/atom.xml", state.base_url), - base_url: &state.base_url, + feed_url: &format!("{}atom.xml", state.base_url), + base_url: &state.base_url.to_string(), last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()), tag: None, posts: &posts, @@ -51,8 +51,8 @@ pub fn render_atom_tag_feed(tag: &Tag, state: &AppState) -> Result { let updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten(); let slug = &tag.slug; let feed = FeedContext { - feed_url: &format!("{}/tags/{}/atom.xml", state.base_url, slug), - base_url: &state.base_url, + feed_url: &format!("{}tags/{}/atom.xml", state.base_url, slug), + base_url: &state.base_url.to_string(), last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()), tag: Some(tag), posts: &posts, @@ -63,14 +63,14 @@ pub fn render_atom_tag_feed(tag: &Tag, state: &AppState) -> Result { Ok(state.tera.render("atom.xml", &ctx)?) } -struct JsonFeed<'a> { +struct _JsonFeed<'a> { version: &'a str, title: &'a str, home_page_url: &'a str, feed_url: &'a str, - items: Vec>, + items: Vec<_JsonFeedItem<'a>>, } -struct JsonFeedItem<'a> { +struct _JsonFeedItem<'a> { id: &'a str, } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 15496b7..2f9769f 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,16 +1,13 @@ use axum::{ body, extract::State, + http::{header, HeaderMap, Request, StatusCode}, middleware::Next, response::{Html, IntoResponse, Response}, routing::get, Router, }; use chrono::{DateTime, FixedOffset}; -use hyper::{ - header::{self, CONTENT_TYPE}, - HeaderMap, Request, StatusCode, -}; use lazy_static::lazy_static; use prometheus::{opts, Encoder, IntCounterVec, TextEncoder}; use std::sync::Arc; @@ -59,7 +56,11 @@ pub fn routes(state: &Arc) -> Router> { #[instrument(skip(state))] pub async fn index( State(state): State>, + headers: HeaderMap, ) -> std::result::Result { + if should_return_304(&headers, Some(state.startup_time.into())) { + return Ok(StatusCode::NOT_MODIFIED.into_response()); + } let ctx = tera::Context::new(); let res = state.tera.render("index.html", &ctx).map_err(|e| { error!("Failed rendering index: {}", e); @@ -85,7 +86,7 @@ async fn metrics() -> impl IntoResponse { Response::builder() .status(200) - .header(CONTENT_TYPE, encoder.format_type()) + .header(header::CONTENT_TYPE, encoder.format_type()) .body(body::boxed(body::Full::from(buffer))) .unwrap() } @@ -165,16 +166,18 @@ mod tests { #[tokio::test] async fn setup_routes() { + let mut state = AppState { + startup_time: chrono::offset::Utc::now(), + base_url: "http://localhost:8180".parse().unwrap(), + tera: tera::Tera::new("templates/**/*").unwrap(), + ..Default::default() + }; // Load the actual posts, just to make this test fail if // aliases overlap with themselves or other routes - let posts = crate::post::load_all().await.unwrap(); - let state = Arc::new(AppState { - startup_time: chrono::offset::Utc::now(), - base_url: "http://localhost:8180".into(), - tera: tera::Tera::new("templates/**/*").unwrap(), - tags: crate::tag::get_tags(posts.values()), - posts, - }); + let posts = crate::post::load_all(&state).await.unwrap(); + state.tags = crate::tag::get_tags(posts.values()); + state.posts = posts; + let state = Arc::new(state); super::routes(&state).with_state(state).into_make_service(); } diff --git a/src/handlers/posts.rs b/src/handlers/posts.rs index cbe81cf..254c35a 100644 --- a/src/handlers/posts.rs +++ b/src/handlers/posts.rs @@ -2,14 +2,12 @@ use std::sync::Arc; use axum::{ extract::{Path, State}, + http::{header, HeaderMap, StatusCode}, response::{Html, IntoResponse, Redirect, Response}, routing::get, Router, }; -use hyper::{ - header::{self, CONTENT_TYPE}, - HeaderMap, StatusCode, -}; + use serde_derive::Serialize; use tracing::{instrument, log::warn}; @@ -74,7 +72,7 @@ pub async fn index( let mut c = tera::Context::new(); c.insert("page", &ctx); c.insert("posts", &posts); - c.insert("base_url", &state.base_url); + c.insert("base_url", &state.base_url.to_string()); let res = state.tera.render("posts_index.html", &c)?; @@ -152,7 +150,7 @@ pub async fn feed( Ok(( StatusCode::OK, [ - (CONTENT_TYPE, "application/atom+xml"), + (header::CONTENT_TYPE, "application/atom+xml"), ( header::LAST_MODIFIED, &last_changed.map_or_else( diff --git a/src/handlers/tags.rs b/src/handlers/tags.rs index eb181f5..c20d15e 100644 --- a/src/handlers/tags.rs +++ b/src/handlers/tags.rs @@ -2,14 +2,12 @@ use std::sync::Arc; use axum::{ extract::{Path, State}, + http::{header, HeaderMap, StatusCode}, response::{Html, IntoResponse, Redirect, Response}, routing::get, Router, }; -use hyper::{ - header::{self, CONTENT_TYPE}, - HeaderMap, StatusCode, -}; + use serde_derive::Serialize; use tracing::instrument; @@ -122,7 +120,7 @@ pub async fn feed( Ok(( StatusCode::OK, [ - (CONTENT_TYPE, "application/atom+xml"), + (header::CONTENT_TYPE, "application/atom+xml"), ( header::LAST_MODIFIED, &last_changed.map_or_else( diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..fdee416 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,33 @@ +use axum::http::{uri, Uri}; + +pub fn uri_with_path(uri: &Uri, path: &str) -> Uri { + if path.starts_with('/') { + // 'path' is an root path, so let's just override the uri's path + return uri::Builder::new() + .scheme(uri.scheme_str().unwrap()) + .authority(uri.authority().unwrap().as_str()) + .path_and_query(path) + .build() + .unwrap(); + } + + // 'path' is a relative/local path, so let's combine it with the uri's path + let base_path = uri.path_and_query().map_or("/", |p| p.path()); + + if base_path.ends_with('/') { + return uri::Builder::new() + .scheme(uri.scheme_str().unwrap()) + .authority(uri.authority().unwrap().as_str()) + .path_and_query(format!("{base_path}{path}")) + .build() + .unwrap(); + } + + let (base, _) = base_path.rsplit_once('/').unwrap(); + return uri::Builder::new() + .scheme(uri.scheme_str().unwrap()) + .authority(uri.authority().unwrap().as_str()) + .path_and_query(format!("{base}/{path}")) + .build() + .unwrap(); +} diff --git a/src/main.rs b/src/main.rs index 2e88b61..8513a84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,15 @@ #![allow(clippy::unused_async)] // axum handlers needs async, even if no awaiting happens use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration}; -use axum::extract::MatchedPath; +use axum::{ + body::Body, + extract::{MatchedPath, OriginalUri}, + http::{uri::PathAndQuery, Request, Uri}, + response::Response, +}; use chrono::DateTime; use color_eyre::eyre::{Error, Result}; -use hyper::{Body, Request, Response}; use post::Post; use tag::Tag; @@ -18,6 +22,7 @@ use tracing_subscriber::{prelude::*, EnvFilter}; mod feed; mod handlers; +mod helpers; mod hilighting; mod markdown; mod post; @@ -26,7 +31,7 @@ mod tag; #[derive(Default)] pub struct AppState { startup_time: DateTime, - base_url: String, + base_url: Uri, posts: HashMap, tags: HashMap, tera: Tera, @@ -39,19 +44,22 @@ async fn main() -> Result<()> { info!("Starting server..."); - let base_url = option_env!("SITE_BASE_URL") + let base_url: Uri = option_env!("SITE_BASE_URL") .unwrap_or("http://localhost:8080") - .to_string(); + .parse() + .unwrap(); let tera = Tera::new("templates/**/*")?; - let posts = post::load_all().await?; - let tags = tag::get_tags(posts.values()); - let state = Arc::new(AppState { + let mut state = AppState { startup_time: chrono::offset::Utc::now(), base_url, tera, - posts, - tags, - }); + ..Default::default() + }; + let posts = post::load_all(&state).await?; + let tags = tag::get_tags(posts.values()); + state.posts = posts; + state.tags = tags; + let state = Arc::new(state); let app = handlers::routes(&state) .layer(CorsLayer::permissive()) @@ -84,18 +92,19 @@ fn init_tracing() { } fn make_span(request: &Request) -> Span { - let uri = request.uri(); + let uri = if let Some(OriginalUri(uri)) = request.extensions().get::() { + uri + } else { + request.uri() + }; let route = request .extensions() .get::() - .map(axum::extract::MatchedPath::as_str) - .unwrap_or_default(); + .map_or(uri.path(), axum::extract::MatchedPath::as_str); let method = request.method().as_str(); let target = uri .path_and_query() - .map(axum::http::uri::PathAndQuery::as_str) - .unwrap_or_default(); - + .map_or(uri.path(), PathAndQuery::as_str); let name = format!("{method} {route}"); info_span!( @@ -108,7 +117,7 @@ fn make_span(request: &Request) -> Span { ) } -fn on_response(response: &Response, _latency: Duration, span: &Span) { +fn on_response(response: &Response, _latency: Duration, span: &Span) { span.record("http.status_code", response.status().as_str()); } diff --git a/src/markdown.rs b/src/markdown.rs index 5bbec22..c93400d 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -1,10 +1,11 @@ +use crate::helpers; +use crate::hilighting; +use axum::http::Uri; use pulldown_cmark::Event; use pulldown_cmark::Tag; use pulldown_cmark::{Options, Parser}; -use crate::hilighting; - -pub fn render_markdown_to_html(markdown: &str) -> String { +pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String { let options = Options::all(); let mut content_html = String::new(); let parser = Parser::new_ext(markdown, options); @@ -22,6 +23,24 @@ pub fn render_markdown_to_html(markdown: &str) -> String { events.push(Event::Text(text)); } } + Event::Start(Tag::Link(t, mut link, title)) => { + if let Some(uri) = base_uri { + if !link.contains("://") && !link.contains('@') { + // convert relative URIs to absolute URIs + link = helpers::uri_with_path(uri, &link).to_string().into(); + } + } + events.push(Event::Start(Tag::Link(t, link, title))); + } + Event::Start(Tag::Image(t, mut link, title)) => { + if let Some(uri) = base_uri { + if !link.contains("://") && !link.contains('@') { + // convert relative URIs to absolute URIs + link = helpers::uri_with_path(uri, &link).to_string().into(); + } + } + events.push(Event::Start(Tag::Image(t, link, title))); + } Event::Start(Tag::CodeBlock(kind)) => { code_block = true; if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind { diff --git a/src/post.rs b/src/post.rs index 984f70b..02da51b 100644 --- a/src/post.rs +++ b/src/post.rs @@ -14,7 +14,7 @@ use tracing::{ log::{debug, warn}, }; -use crate::{markdown, AppState, WebsiteError}; +use crate::{helpers, markdown, AppState, WebsiteError}; #[derive(Deserialize, Debug, Default)] pub struct TomlFrontMatter { @@ -69,8 +69,8 @@ impl Post { } } -#[instrument] -pub async fn load_all() -> color_eyre::eyre::Result> { +#[instrument(skip(state))] +pub async fn load_all(state: &AppState) -> color_eyre::eyre::Result> { let mut res = HashMap::::new(); for path in glob("posts/**/*.md")? { let path = path.unwrap(); @@ -87,15 +87,15 @@ pub async fn load_all() -> color_eyre::eyre::Result> { .trim_end_matches('\\') .trim_end_matches('/'); - let post = load_post(slug).await?; + let post = load_post(state, slug).await?; res.insert(slug.to_string(), post); } Ok(res) } -#[instrument] -pub async fn load_post(slug: &str) -> color_eyre::eyre::Result { +#[instrument(skip(state))] +pub async fn load_post(state: &AppState, slug: &str) -> color_eyre::eyre::Result { debug!("loading post: {slug}"); let file_path = Path::new("posts").join(slug); @@ -109,7 +109,9 @@ pub async fn load_post(slug: &str) -> color_eyre::eyre::Result { let (tomlfm, content) = parse_frontmatter(content)?; let tomlfm = tomlfm.expect("Missing frontmatter"); - let content = content.map(|c| markdown::render_markdown_to_html(&c)); + let base_uri = helpers::uri_with_path(&state.base_url, &format!("/posts/{slug}/")); + + let content = content.map(|c| markdown::render_markdown_to_html(Some(&base_uri), &c)); Ok(Post::new( slug.to_string(), @@ -143,7 +145,7 @@ fn parse_frontmatter( pub async fn render_post(state: &AppState, post: &Post) -> Result { let mut ctx = tera::Context::new(); ctx.insert("page", &post); - ctx.insert("base_url", &state.base_url); + ctx.insert("base_url", &state.base_url.to_string()); state .tera @@ -159,12 +161,12 @@ mod tests { #[tokio::test] async fn render_all_posts() { - let state = AppState { - base_url: "localhost:8180".into(), - posts: super::load_all().await.unwrap(), + let mut state = AppState { + base_url: "localhost:8180".parse().unwrap(), tera: Tera::new("templates/**/*").unwrap(), ..Default::default() }; + state.posts = super::load_all(&state).await.unwrap(); for post in state.posts.values() { super::render_post(&state, post).await.unwrap(); } diff --git a/templates/atom.xml b/templates/atom.xml index 0cd0708..d1fd6ad 100644 --- a/templates/atom.xml +++ b/templates/atom.xml @@ -6,7 +6,7 @@ tollyx's corner of the web {% if tag -%} - + {%- else -%} {%- endif %} @@ -21,9 +21,11 @@ tollyx - - {{ post.slug | safe }} - {{ post.content }} + + {{ base_url | trim_end_matches(pat='/') | safe }}{{ post.absolute_path | safe }} + + {{ post.content }} + {%- endfor %} \ No newline at end of file diff --git a/templates/partials/head.html b/templates/partials/head.html index 387f686..9685aa9 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -2,12 +2,12 @@ - + {% if tag_slug -%} - + {%- endif %} - - + + {% if page -%} @@ -17,7 +17,7 @@ {%- else -%} {%- endif %} - + diff --git a/templates/partials/header.html b/templates/partials/header.html index bc22bed..328de26 100644 --- a/templates/partials/header.html +++ b/templates/partials/header.html @@ -1,3 +1,3 @@ diff --git a/templates/post.html b/templates/post.html index 9c2a23e..ce9f752 100644 --- a/templates/post.html +++ b/templates/post.html @@ -16,7 +16,7 @@ {% if page.tags -%}
    - {% for tag in page.tags %}
  • #{{ tag }}
  • {% endfor %} + {% for tag in page.tags %}
  • #{{ tag }}
  • {% endfor %}
{%- endif %} diff --git a/templates/posts_index.html b/templates/posts_index.html index 4645358..30acb5a 100644 --- a/templates/posts_index.html +++ b/templates/posts_index.html @@ -4,7 +4,7 @@

I occasionally write some stuff, it's quite rare but it does happen believe it or not.