From 6a5a9c890fd181da4ef1ce1e0bafa23d6069490c Mon Sep 17 00:00:00 2001 From: Michal Vanko Date: Mon, 16 Jun 2025 21:15:53 +0200 Subject: [PATCH] migrate axum and askama --- Cargo.toml | 3 +-- src/feed.rs | 17 +++++++++++---- src/filters/markdown.rs | 9 ++++++-- src/filters/pretty_date.rs | 2 +- src/filters/truncate_md.rs | 2 +- src/pages/admin.rs | 12 +++++++++-- src/pages/animated_logo.rs | 9 +++++--- src/pages/blog_post_list.rs | 28 +++++++++++++++---------- src/pages/blog_post_page.rs | 29 +++++++++++++++----------- src/pages/broadcast_list.rs | 28 +++++++++++++++---------- src/pages/contact.rs | 21 ++++++++++++------- src/pages/index.rs | 27 +++++++++++++++--------- src/pages/not_found.rs | 20 ++++++++++++------ src/pages/portfolio.rs | 31 +++++++++++++++++----------- src/pages/project_list.rs | 21 ++++++++++++------- src/pages/showcase/egg_fetcher.rs | 17 ++++++++++----- src/router.rs | 19 +++++++++++------ styles/output.css | 8 ------- templates/components/skill_card.html | 3 +-- 19 files changed, 194 insertions(+), 112 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c704d99..a336c82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -askama = { version = "0.14", features = ["with-axum", "mime", "mime_guess"] } -askama_axum = "0.5.0" +askama = { version = "0.14" } axum = "0.8.0" chrono = { version = "0.4.31", features = ["serde"] } pulldown-cmark = { version = "0.13" } diff --git a/src/feed.rs b/src/feed.rs index 7a573e5..5f2450c 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -1,3 +1,4 @@ +use askama::Values; use axum::http::{header, StatusCode}; use axum::response::IntoResponse; use chrono::Utc; @@ -7,6 +8,14 @@ use crate::blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH}; use crate::filters::{parse_markdown, truncate_md}; use crate::post_utils::post_listing::get_post_list; +struct EmptyValues; + +impl Values for EmptyValues { + fn get_value<'a>(&'a self, _key: &str) -> Option<&'a dyn std::any::Any> { + return None; + } +} + pub async fn render_rss_feed() -> Result { let mut post_list = get_post_list::(BLOG_POST_PATH) .await @@ -27,14 +36,14 @@ pub async fn render_rss_feed() -> Result { .title(Some(post.metadata.title)) .link(Some(format!("https://michalvanko.dev/blog/{}", post.slug))) .description({ - let truncated = - truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string()); - let parsed_md = parse_markdown(&truncated) + let truncated = truncate_md(&post.body, &EmptyValues, 2) + .unwrap_or("Can't parse post body".to_string()); + let parsed_md = parse_markdown(&truncated, &EmptyValues) .unwrap_or("Can't process truncated post body".to_string()); Some(parsed_md) }) .content({ - let parsed_md = parse_markdown(&post.body) + let parsed_md = parse_markdown(&post.body, &EmptyValues) .unwrap_or("Can't process full post body".to_string()); Some(parsed_md) }) diff --git a/src/filters/markdown.rs b/src/filters/markdown.rs index 4c09600..5582038 100644 --- a/src/filters/markdown.rs +++ b/src/filters/markdown.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::path::Path; use image::image_dimensions; @@ -19,7 +20,10 @@ enum TextKind { } // pub fn parse_markdown(markdown: &str) -> ::askama::Result -pub fn parse_markdown(markdown: &str) -> ::askama::Result { +pub fn parse_markdown( + markdown: T, + _: &dyn askama::Values, +) -> ::askama::Result { let mut options = Options::empty(); options.insert(Options::ENABLE_TABLES); options.insert(Options::ENABLE_FOOTNOTES); @@ -34,7 +38,8 @@ pub fn parse_markdown(markdown: &str) -> ::askama::Result { let theme = theme_set.themes.get("InspiredGitHub").unwrap(); let mut heading_ended: Option = None; - let parser = Parser::new_ext(markdown, options).map(|event| match event { + let mds = markdown.to_string(); + let parser = Parser::new_ext(&mds, options).map(|event| match event { /* Parsing images considers `alt` attribute as inner `Text` event Therefore the `[alt]` is rendered in html as subtitle diff --git a/src/filters/pretty_date.rs b/src/filters/pretty_date.rs index 73a16b6..8007913 100644 --- a/src/filters/pretty_date.rs +++ b/src/filters/pretty_date.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; // This filter does not have extra arguments -pub fn pretty_date(date_time: &DateTime) -> ::askama::Result { +pub fn pretty_date(date_time: &DateTime, _: &dyn askama::Values) -> ::askama::Result { let formatted = format!("{}", date_time.format("%e %B %Y")); Ok(formatted) } diff --git a/src/filters/truncate_md.rs b/src/filters/truncate_md.rs index d7f59cd..ecde5d5 100644 --- a/src/filters/truncate_md.rs +++ b/src/filters/truncate_md.rs @@ -2,7 +2,7 @@ const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"]; -pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result { +pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result { let description = body .lines() .filter(|line| { diff --git a/src/pages/admin.rs b/src/pages/admin.rs index fd04e61..b617a14 100644 --- a/src/pages/admin.rs +++ b/src/pages/admin.rs @@ -1,9 +1,17 @@ use askama::Template; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; #[derive(Template)] #[template(path = "admin.html")] pub struct AdminPageTemplate {} -pub async fn render_admin() -> AdminPageTemplate { - AdminPageTemplate {} +pub async fn render_admin() -> Result { + Ok(Html( + AdminPageTemplate {} + .render() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR), + )) } diff --git a/src/pages/animated_logo.rs b/src/pages/animated_logo.rs index 2030333..fd65ac3 100644 --- a/src/pages/animated_logo.rs +++ b/src/pages/animated_logo.rs @@ -1,10 +1,13 @@ use askama::Template; -use axum::http::StatusCode; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; #[derive(Template)] #[template(path = "assets/animated_logo.html")] pub struct AnimatedLogoTemplate {} -pub async fn render_animated_logo() -> Result { - Ok(AnimatedLogoTemplate {}) +pub async fn render_animated_logo() -> Result { + Ok(Html(AnimatedLogoTemplate {}.render().unwrap())) } diff --git a/src/pages/blog_post_list.rs b/src/pages/blog_post_list.rs index 643bf4a..80beaf8 100644 --- a/src/pages/blog_post_list.rs +++ b/src/pages/blog_post_list.rs @@ -1,6 +1,8 @@ +use askama::Template; use axum::{ extract::{OriginalUri, Path}, http::StatusCode, + response::{Html, IntoResponse}, }; use tokio::try_join; use tracing::debug; @@ -22,7 +24,7 @@ use super::post_list::PostListTemplate; pub async fn render_blog_post_list( tag: Option>, OriginalUri(original_uri): OriginalUri, -) -> Result { +) -> Result { // I will forget what happens here in a week. But essentially it's pattern matching and shadowing let tag = tag.map(|Path(tag)| tag); @@ -50,14 +52,18 @@ pub async fn render_blog_post_list( ("Blog posts".to_string(), "Blog posts".to_string()) }; - Ok(PostListTemplate { - title, - og_title, - segment: Segment::Blog, - posts, - header_props, - tags: blog_tags, - featured_projects, - current_url: original_uri.to_string(), - }) + Ok(Html( + PostListTemplate { + title, + og_title, + segment: Segment::Blog, + posts, + header_props, + tags: blog_tags, + featured_projects, + current_url: original_uri.to_string(), + } + .render() + .unwrap(), + )) } diff --git a/src/pages/blog_post_page.rs b/src/pages/blog_post_page.rs index f425008..3968376 100644 --- a/src/pages/blog_post_page.rs +++ b/src/pages/blog_post_page.rs @@ -1,5 +1,6 @@ use askama::Template; use axum::extract::OriginalUri; +use axum::response::{Html, IntoResponse}; use axum::{extract::Path, http::StatusCode}; use chrono::{DateTime, Utc}; @@ -30,7 +31,7 @@ pub struct BlogPostTemplate { pub async fn render_blog_post( Path(post_id): Path, OriginalUri(original_uri): OriginalUri, -) -> Result { +) -> Result { let path = format!("{}/{}.md", BLOG_POST_PATH, post_id); let post = parse_post::(&path).await?; let segment = if original_uri.to_string().starts_with("/blog") { @@ -59,17 +60,21 @@ pub async fn render_blog_post( _ => HeaderProps::default(), }; - Ok(BlogPostTemplate { - title: post.metadata.title, - date: post.metadata.date, - tags: post.metadata.tags, - body: post.body, - slug: post.slug, - segment, - thumbnail: post.metadata.thumbnail, - header_props, - recommended_posts, - }) + Ok(Html( + BlogPostTemplate { + title: post.metadata.title, + date: post.metadata.date, + tags: post.metadata.tags, + body: post.body, + slug: post.slug, + segment, + thumbnail: post.metadata.thumbnail, + header_props, + recommended_posts, + } + .render() + .unwrap(), + )) } async fn get_recommended_posts( diff --git a/src/pages/broadcast_list.rs b/src/pages/broadcast_list.rs index cb24a82..df515c6 100644 --- a/src/pages/broadcast_list.rs +++ b/src/pages/broadcast_list.rs @@ -1,6 +1,8 @@ +use askama::Template; use axum::{ extract::{OriginalUri, Path}, http::StatusCode, + response::{Html, IntoResponse}, }; use tokio::try_join; use tracing::debug; @@ -21,7 +23,7 @@ use super::post_list::PostListTemplate; pub async fn render_broadcast_post_list( tag: Option>, OriginalUri(original_uri): OriginalUri, -) -> Result { +) -> Result { // I will forget what happens here in a week. But essentially it's pattern matching and shadowing let tag = tag.map(|Path(tag)| tag); @@ -50,14 +52,18 @@ pub async fn render_broadcast_post_list( "Broadcasts".to_string() }; - Ok(PostListTemplate { - title: title.clone(), - og_title: title, - segment: Segment::Broadcasts, - posts, - header_props, - tags: popular_tags, - featured_projects, - current_url: original_uri.to_string(), - }) + Ok(Html( + PostListTemplate { + title: title.clone(), + og_title: title, + segment: Segment::Broadcasts, + posts, + header_props, + tags: popular_tags, + featured_projects, + current_url: original_uri.to_string(), + } + .render() + .unwrap(), + )) } diff --git a/src/pages/contact.rs b/src/pages/contact.rs index 080d9cf..0fe2b83 100644 --- a/src/pages/contact.rs +++ b/src/pages/contact.rs @@ -1,5 +1,8 @@ use askama::Template; -use axum::http::StatusCode; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; use crate::components::site_header::HeaderProps; @@ -18,7 +21,7 @@ pub struct ContactPageTemplate { pub links: Vec, } -pub async fn render_contact() -> Result { +pub async fn render_contact() -> Result { let links = vec![ ContactLink { href: "mailto:michalvankosk@gmail.com".to_string(), @@ -76,9 +79,13 @@ pub async fn render_contact() -> Result { }, ]; - Ok(ContactPageTemplate { - title: "Contact".to_owned(), - header_props: HeaderProps::default(), - links, - }) + Ok(Html( + ContactPageTemplate { + title: "Contact".to_owned(), + header_props: HeaderProps::default(), + links, + } + .render() + .unwrap(), + )) } diff --git a/src/pages/index.rs b/src/pages/index.rs index 0e2f919..64e9704 100644 --- a/src/pages/index.rs +++ b/src/pages/index.rs @@ -1,7 +1,10 @@ use std::rc::Rc; use askama::Template; -use axum::http::StatusCode; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; use tokio::try_join; use crate::{ @@ -26,7 +29,7 @@ pub struct IndexTemplate { featured_broadcasts: Vec>>, } -pub async fn render_index() -> Result { +pub async fn render_index() -> Result { let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!( get_popular_tags(Some(Segment::Blog)), get_popular_tags(Some(Segment::Broadcasts)), @@ -44,12 +47,16 @@ pub async fn render_index() -> Result { let featured_broadcasts = ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]); - Ok(IndexTemplate { - header_props: HeaderProps::default(), - blog_tags, - broadcasts_tags, - featured_blog_posts, - featured_projects, - featured_broadcasts, - }) + Ok(Html( + IndexTemplate { + header_props: HeaderProps::default(), + blog_tags, + broadcasts_tags, + featured_blog_posts, + featured_projects, + featured_broadcasts, + } + .render() + .unwrap(), + )) } diff --git a/src/pages/not_found.rs b/src/pages/not_found.rs index 17df806..367e0a6 100644 --- a/src/pages/not_found.rs +++ b/src/pages/not_found.rs @@ -1,5 +1,9 @@ use askama::Template; -use axum::{extract::OriginalUri, http::StatusCode}; +use axum::{ + extract::OriginalUri, + http::StatusCode, + response::{Html, IntoResponse}, +}; use tracing::info; use crate::components::site_header::HeaderProps; @@ -13,13 +17,17 @@ pub struct NotFoundPage { pub async fn render_not_found( OriginalUri(original_uri): OriginalUri, -) -> Result<(StatusCode, NotFoundPage), StatusCode> { +) -> Result<(StatusCode, impl IntoResponse), StatusCode> { info!("{original_uri} not found"); Ok(( StatusCode::NOT_FOUND, - NotFoundPage { - title: "This page does not exists".to_owned(), - header_props: HeaderProps::default(), - }, + Html( + NotFoundPage { + title: "This page does not exists".to_owned(), + header_props: HeaderProps::default(), + } + .render() + .unwrap(), + ), )) } diff --git a/src/pages/portfolio.rs b/src/pages/portfolio.rs index e7675f6..6517186 100644 --- a/src/pages/portfolio.rs +++ b/src/pages/portfolio.rs @@ -1,5 +1,8 @@ use askama::Template; -use axum::http::StatusCode; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; use serde::Deserialize; use crate::{ @@ -50,7 +53,7 @@ pub struct PortfolioTemplate { pub technology_list: Vec, } -pub async fn render_portfolio() -> Result { +pub async fn render_portfolio() -> Result { let portfolio = parse_post::("_pages/portfolio.md").await?; let mut project_list = get_post_list::("_projects").await?; @@ -126,14 +129,18 @@ pub async fn render_portfolio() -> Result { .map(|str| str.to_owned()) .collect(); - Ok(PortfolioTemplate { - title: "Portfolio".to_owned(), - body: portfolio.body, - header_props: HeaderProps::default(), - project_list, - workplace_list, - education_list, - contact_links, - technology_list, - }) + Ok(Html( + PortfolioTemplate { + title: "Portfolio".to_owned(), + body: portfolio.body, + header_props: HeaderProps::default(), + project_list, + workplace_list, + education_list, + contact_links, + technology_list, + } + .render() + .unwrap(), + )) } diff --git a/src/pages/project_list.rs b/src/pages/project_list.rs index e4d0477..e1cba5e 100644 --- a/src/pages/project_list.rs +++ b/src/pages/project_list.rs @@ -1,5 +1,8 @@ use askama::Template; -use axum::http::StatusCode; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; use crate::{ components::site_header::HeaderProps, @@ -16,16 +19,20 @@ pub struct ProjectListTemplate { pub header_props: HeaderProps, } -pub async fn render_projects_list() -> Result { +pub async fn render_projects_list() -> Result { let mut project_list = get_post_list::("_projects").await?; project_list.sort_by_key(|post| post.slug.to_string()); project_list.retain(|project| project.metadata.displayed); project_list.reverse(); - Ok(ProjectListTemplate { - title: "Showcase".to_owned(), - header_props: HeaderProps::default(), - project_list, - }) + Ok(Html( + ProjectListTemplate { + title: "Showcase".to_owned(), + header_props: HeaderProps::default(), + project_list, + } + .render() + .unwrap(), + )) } diff --git a/src/pages/showcase/egg_fetcher.rs b/src/pages/showcase/egg_fetcher.rs index 369ad25..147e94a 100644 --- a/src/pages/showcase/egg_fetcher.rs +++ b/src/pages/showcase/egg_fetcher.rs @@ -1,5 +1,8 @@ use askama::Template; -use axum::http::StatusCode; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse}, +}; use crate::components::site_header::HeaderProps; @@ -9,8 +12,12 @@ pub struct EggFetcherShowcaseTemplate { header_props: HeaderProps, } -pub async fn render_egg_fetcher() -> Result { - Ok(EggFetcherShowcaseTemplate { - header_props: HeaderProps::default(), - }) +pub async fn render_egg_fetcher() -> Result { + Ok(Html( + EggFetcherShowcaseTemplate { + header_props: HeaderProps::default(), + } + .render() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR), + )) } diff --git a/src/router.rs b/src/router.rs index 92b893c..ab330a8 100644 --- a/src/router.rs +++ b/src/router.rs @@ -8,7 +8,14 @@ use crate::{ project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher, }, }; -use axum::{extract::MatchedPath, http::Request, routing::get, Router}; +use askama::Template; +use axum::{ + extract::MatchedPath, + http::{Request, StatusCode}, + response::IntoResponse, + routing::get, + Router, +}; use tower_http::trace::TraceLayer; use tracing::info_span; @@ -16,15 +23,15 @@ pub fn get_router() -> Router { Router::new() .route("/", get(render_index)) .route("/blog", get(render_blog_post_list)) - .route("/blog/tags/:tag", get(render_blog_post_list)) - .route("/blog/:post_id", get(render_blog_post)) + .route("/blog/tags/{tag}", get(render_blog_post_list)) + .route("/blog/{post_id}", get(render_blog_post)) .route("/broadcasts", get(render_broadcast_post_list)) - .route("/broadcasts/tags/:tag", get(render_broadcast_post_list)) - .route("/broadcasts/:post_id", get(render_blog_post)) + .route("/broadcasts/tags/{tag}", get(render_broadcast_post_list)) + .route("/broadcasts/{post_id}", get(render_blog_post)) .route("/contact", get(render_contact)) .route("/showcase", get(render_projects_list)) .route("/showcase/m-logo-svg", get(render_animated_logo)) - .route("/showcase/:project_slug", get(render_egg_fetcher)) + .route("/showcase/{project_slug}", get(render_egg_fetcher)) .route("/portfolio", get(render_portfolio)) .route("/admin", get(render_admin)) .route("/feed.xml", get(render_rss_feed)) diff --git a/styles/output.css b/styles/output.css index edf8bd5..fa64377 100644 --- a/styles/output.css +++ b/styles/output.css @@ -335,9 +335,6 @@ .mt-8 { margin-top: calc(var(--spacing) * 8); } - .mt-12 { - margin-top: calc(var(--spacing) * 12); - } .mr-3 { margin-right: calc(var(--spacing) * 3); } @@ -936,11 +933,6 @@ margin-block: calc(var(--spacing) * 6); } } - .lg\:mt-8 { - @media (width >= 64rem) { - margin-top: calc(var(--spacing) * 8); - } - } .lg\:mt-10 { @media (width >= 64rem) { margin-top: calc(var(--spacing) * 10); diff --git a/templates/components/skill_card.html b/templates/components/skill_card.html index 486be4f..8f259d4 100644 --- a/templates/components/skill_card.html +++ b/templates/components/skill_card.html @@ -2,8 +2,7 @@