migrate axum and askama
Some checks failed
test / cargo test (push) Failing after 1m10s

This commit is contained in:
2025-06-16 21:15:53 +02:00
parent 5ba193cd56
commit 6a5a9c890f
19 changed files with 194 additions and 112 deletions

View File

@ -6,8 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
askama = { version = "0.14", features = ["with-axum", "mime", "mime_guess"] } askama = { version = "0.14" }
askama_axum = "0.5.0"
axum = "0.8.0" axum = "0.8.0"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
pulldown-cmark = { version = "0.13" } pulldown-cmark = { version = "0.13" }

View File

@ -1,3 +1,4 @@
use askama::Values;
use axum::http::{header, StatusCode}; use axum::http::{header, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use chrono::Utc; 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::filters::{parse_markdown, truncate_md};
use crate::post_utils::post_listing::get_post_list; 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<impl IntoResponse, StatusCode> { pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH) let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
.await .await
@ -27,14 +36,14 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
.title(Some(post.metadata.title)) .title(Some(post.metadata.title))
.link(Some(format!("https://michalvanko.dev/blog/{}", post.slug))) .link(Some(format!("https://michalvanko.dev/blog/{}", post.slug)))
.description({ .description({
let truncated = let truncated = truncate_md(&post.body, &EmptyValues, 2)
truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string()); .unwrap_or("Can't parse post body".to_string());
let parsed_md = parse_markdown(&truncated) let parsed_md = parse_markdown(&truncated, &EmptyValues)
.unwrap_or("Can't process truncated post body".to_string()); .unwrap_or("Can't process truncated post body".to_string());
Some(parsed_md) Some(parsed_md)
}) })
.content({ .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()); .unwrap_or("Can't process full post body".to_string());
Some(parsed_md) Some(parsed_md)
}) })

View File

@ -1,3 +1,4 @@
use core::fmt;
use std::path::Path; use std::path::Path;
use image::image_dimensions; use image::image_dimensions;
@ -19,7 +20,10 @@ enum TextKind {
} }
// pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> // pub fn parse_markdown(markdown: &str) -> ::askama::Result<String>
pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> { pub fn parse_markdown<T: fmt::Display>(
markdown: T,
_: &dyn askama::Values,
) -> ::askama::Result<String> {
let mut options = Options::empty(); let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES); options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES); options.insert(Options::ENABLE_FOOTNOTES);
@ -34,7 +38,8 @@ pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
let theme = theme_set.themes.get("InspiredGitHub").unwrap(); let theme = theme_set.themes.get("InspiredGitHub").unwrap();
let mut heading_ended: Option<bool> = None; let mut heading_ended: Option<bool> = 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 Parsing images considers `alt` attribute as inner `Text` event
Therefore the `[alt]` is rendered in html as subtitle Therefore the `[alt]` is rendered in html as subtitle

View File

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
// This filter does not have extra arguments // This filter does not have extra arguments
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> { pub fn pretty_date(date_time: &DateTime<Utc>, _: &dyn askama::Values) -> ::askama::Result<String> {
let formatted = format!("{}", date_time.format("%e %B %Y")); let formatted = format!("{}", date_time.format("%e %B %Y"));
Ok(formatted) Ok(formatted)
} }

View File

@ -2,7 +2,7 @@
const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"]; const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"];
pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result<String> { pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result<String> {
let description = body let description = body
.lines() .lines()
.filter(|line| { .filter(|line| {

View File

@ -1,9 +1,17 @@
use askama::Template; use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
#[derive(Template)] #[derive(Template)]
#[template(path = "admin.html")] #[template(path = "admin.html")]
pub struct AdminPageTemplate {} pub struct AdminPageTemplate {}
pub async fn render_admin() -> AdminPageTemplate { pub async fn render_admin() -> Result<impl IntoResponse, StatusCode> {
AdminPageTemplate {} Ok(Html(
AdminPageTemplate {}
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
))
} }

View File

@ -1,10 +1,13 @@
use askama::Template; use askama::Template;
use axum::http::StatusCode; use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
#[derive(Template)] #[derive(Template)]
#[template(path = "assets/animated_logo.html")] #[template(path = "assets/animated_logo.html")]
pub struct AnimatedLogoTemplate {} pub struct AnimatedLogoTemplate {}
pub async fn render_animated_logo() -> Result<AnimatedLogoTemplate, StatusCode> { pub async fn render_animated_logo() -> Result<impl IntoResponse, StatusCode> {
Ok(AnimatedLogoTemplate {}) Ok(Html(AnimatedLogoTemplate {}.render().unwrap()))
} }

View File

@ -1,6 +1,8 @@
use askama::Template;
use axum::{ use axum::{
extract::{OriginalUri, Path}, extract::{OriginalUri, Path},
http::StatusCode, http::StatusCode,
response::{Html, IntoResponse},
}; };
use tokio::try_join; use tokio::try_join;
use tracing::debug; use tracing::debug;
@ -22,7 +24,7 @@ use super::post_list::PostListTemplate;
pub async fn render_blog_post_list( pub async fn render_blog_post_list(
tag: Option<Path<String>>, tag: Option<Path<String>>,
OriginalUri(original_uri): OriginalUri, OriginalUri(original_uri): OriginalUri,
) -> Result<PostListTemplate, StatusCode> { ) -> Result<impl IntoResponse, StatusCode> {
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing // I will forget what happens here in a week. But essentially it's pattern matching and shadowing
let tag = tag.map(|Path(tag)| tag); 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()) ("Blog posts".to_string(), "Blog posts".to_string())
}; };
Ok(PostListTemplate { Ok(Html(
title, PostListTemplate {
og_title, title,
segment: Segment::Blog, og_title,
posts, segment: Segment::Blog,
header_props, posts,
tags: blog_tags, header_props,
featured_projects, tags: blog_tags,
current_url: original_uri.to_string(), featured_projects,
}) current_url: original_uri.to_string(),
}
.render()
.unwrap(),
))
} }

View File

@ -1,5 +1,6 @@
use askama::Template; use askama::Template;
use axum::extract::OriginalUri; use axum::extract::OriginalUri;
use axum::response::{Html, IntoResponse};
use axum::{extract::Path, http::StatusCode}; use axum::{extract::Path, http::StatusCode};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@ -30,7 +31,7 @@ pub struct BlogPostTemplate {
pub async fn render_blog_post( pub async fn render_blog_post(
Path(post_id): Path<String>, Path(post_id): Path<String>,
OriginalUri(original_uri): OriginalUri, OriginalUri(original_uri): OriginalUri,
) -> Result<BlogPostTemplate, StatusCode> { ) -> Result<impl IntoResponse, StatusCode> {
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id); let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
let post = parse_post::<BlogPostMetadata>(&path).await?; let post = parse_post::<BlogPostMetadata>(&path).await?;
let segment = if original_uri.to_string().starts_with("/blog") { let segment = if original_uri.to_string().starts_with("/blog") {
@ -59,17 +60,21 @@ pub async fn render_blog_post(
_ => HeaderProps::default(), _ => HeaderProps::default(),
}; };
Ok(BlogPostTemplate { Ok(Html(
title: post.metadata.title, BlogPostTemplate {
date: post.metadata.date, title: post.metadata.title,
tags: post.metadata.tags, date: post.metadata.date,
body: post.body, tags: post.metadata.tags,
slug: post.slug, body: post.body,
segment, slug: post.slug,
thumbnail: post.metadata.thumbnail, segment,
header_props, thumbnail: post.metadata.thumbnail,
recommended_posts, header_props,
}) recommended_posts,
}
.render()
.unwrap(),
))
} }
async fn get_recommended_posts( async fn get_recommended_posts(

View File

@ -1,6 +1,8 @@
use askama::Template;
use axum::{ use axum::{
extract::{OriginalUri, Path}, extract::{OriginalUri, Path},
http::StatusCode, http::StatusCode,
response::{Html, IntoResponse},
}; };
use tokio::try_join; use tokio::try_join;
use tracing::debug; use tracing::debug;
@ -21,7 +23,7 @@ use super::post_list::PostListTemplate;
pub async fn render_broadcast_post_list( pub async fn render_broadcast_post_list(
tag: Option<Path<String>>, tag: Option<Path<String>>,
OriginalUri(original_uri): OriginalUri, OriginalUri(original_uri): OriginalUri,
) -> Result<PostListTemplate, StatusCode> { ) -> Result<impl IntoResponse, StatusCode> {
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing // I will forget what happens here in a week. But essentially it's pattern matching and shadowing
let tag = tag.map(|Path(tag)| tag); let tag = tag.map(|Path(tag)| tag);
@ -50,14 +52,18 @@ pub async fn render_broadcast_post_list(
"Broadcasts".to_string() "Broadcasts".to_string()
}; };
Ok(PostListTemplate { Ok(Html(
title: title.clone(), PostListTemplate {
og_title: title, title: title.clone(),
segment: Segment::Broadcasts, og_title: title,
posts, segment: Segment::Broadcasts,
header_props, posts,
tags: popular_tags, header_props,
featured_projects, tags: popular_tags,
current_url: original_uri.to_string(), featured_projects,
}) current_url: original_uri.to_string(),
}
.render()
.unwrap(),
))
} }

View File

@ -1,5 +1,8 @@
use askama::Template; use askama::Template;
use axum::http::StatusCode; use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use crate::components::site_header::HeaderProps; use crate::components::site_header::HeaderProps;
@ -18,7 +21,7 @@ pub struct ContactPageTemplate {
pub links: Vec<ContactLink>, pub links: Vec<ContactLink>,
} }
pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> { pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
let links = vec![ let links = vec![
ContactLink { ContactLink {
href: "mailto:michalvankosk@gmail.com".to_string(), href: "mailto:michalvankosk@gmail.com".to_string(),
@ -76,9 +79,13 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
}, },
]; ];
Ok(ContactPageTemplate { Ok(Html(
title: "Contact".to_owned(), ContactPageTemplate {
header_props: HeaderProps::default(), title: "Contact".to_owned(),
links, header_props: HeaderProps::default(),
}) links,
}
.render()
.unwrap(),
))
} }

View File

@ -1,7 +1,10 @@
use std::rc::Rc; use std::rc::Rc;
use askama::Template; use askama::Template;
use axum::http::StatusCode; use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use tokio::try_join; use tokio::try_join;
use crate::{ use crate::{
@ -26,7 +29,7 @@ pub struct IndexTemplate {
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>, featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
} }
pub async fn render_index() -> Result<IndexTemplate, StatusCode> { pub async fn render_index() -> Result<impl IntoResponse, StatusCode> {
let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!( let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!(
get_popular_tags(Some(Segment::Blog)), get_popular_tags(Some(Segment::Blog)),
get_popular_tags(Some(Segment::Broadcasts)), get_popular_tags(Some(Segment::Broadcasts)),
@ -44,12 +47,16 @@ pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
let featured_broadcasts = let featured_broadcasts =
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]); ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]);
Ok(IndexTemplate { Ok(Html(
header_props: HeaderProps::default(), IndexTemplate {
blog_tags, header_props: HeaderProps::default(),
broadcasts_tags, blog_tags,
featured_blog_posts, broadcasts_tags,
featured_projects, featured_blog_posts,
featured_broadcasts, featured_projects,
}) featured_broadcasts,
}
.render()
.unwrap(),
))
} }

View File

@ -1,5 +1,9 @@
use askama::Template; use askama::Template;
use axum::{extract::OriginalUri, http::StatusCode}; use axum::{
extract::OriginalUri,
http::StatusCode,
response::{Html, IntoResponse},
};
use tracing::info; use tracing::info;
use crate::components::site_header::HeaderProps; use crate::components::site_header::HeaderProps;
@ -13,13 +17,17 @@ pub struct NotFoundPage {
pub async fn render_not_found( pub async fn render_not_found(
OriginalUri(original_uri): OriginalUri, OriginalUri(original_uri): OriginalUri,
) -> Result<(StatusCode, NotFoundPage), StatusCode> { ) -> Result<(StatusCode, impl IntoResponse), StatusCode> {
info!("{original_uri} not found"); info!("{original_uri} not found");
Ok(( Ok((
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
NotFoundPage { Html(
title: "This page does not exists".to_owned(), NotFoundPage {
header_props: HeaderProps::default(), title: "This page does not exists".to_owned(),
}, header_props: HeaderProps::default(),
}
.render()
.unwrap(),
),
)) ))
} }

View File

@ -1,5 +1,8 @@
use askama::Template; use askama::Template;
use axum::http::StatusCode; use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
@ -50,7 +53,7 @@ pub struct PortfolioTemplate {
pub technology_list: Vec<String>, pub technology_list: Vec<String>,
} }
pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> { pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> {
let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?; let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?;
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?; let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
@ -126,14 +129,18 @@ pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
.map(|str| str.to_owned()) .map(|str| str.to_owned())
.collect(); .collect();
Ok(PortfolioTemplate { Ok(Html(
title: "Portfolio".to_owned(), PortfolioTemplate {
body: portfolio.body, title: "Portfolio".to_owned(),
header_props: HeaderProps::default(), body: portfolio.body,
project_list, header_props: HeaderProps::default(),
workplace_list, project_list,
education_list, workplace_list,
contact_links, education_list,
technology_list, contact_links,
}) technology_list,
}
.render()
.unwrap(),
))
} }

View File

@ -1,5 +1,8 @@
use askama::Template; use askama::Template;
use axum::http::StatusCode; use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use crate::{ use crate::{
components::site_header::HeaderProps, components::site_header::HeaderProps,
@ -16,16 +19,20 @@ pub struct ProjectListTemplate {
pub header_props: HeaderProps, pub header_props: HeaderProps,
} }
pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> { pub async fn render_projects_list() -> Result<impl IntoResponse, StatusCode> {
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?; let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
project_list.sort_by_key(|post| post.slug.to_string()); project_list.sort_by_key(|post| post.slug.to_string());
project_list.retain(|project| project.metadata.displayed); project_list.retain(|project| project.metadata.displayed);
project_list.reverse(); project_list.reverse();
Ok(ProjectListTemplate { Ok(Html(
title: "Showcase".to_owned(), ProjectListTemplate {
header_props: HeaderProps::default(), title: "Showcase".to_owned(),
project_list, header_props: HeaderProps::default(),
}) project_list,
}
.render()
.unwrap(),
))
} }

View File

@ -1,5 +1,8 @@
use askama::Template; use askama::Template;
use axum::http::StatusCode; use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use crate::components::site_header::HeaderProps; use crate::components::site_header::HeaderProps;
@ -9,8 +12,12 @@ pub struct EggFetcherShowcaseTemplate {
header_props: HeaderProps, header_props: HeaderProps,
} }
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> { pub async fn render_egg_fetcher() -> Result<impl IntoResponse, StatusCode> {
Ok(EggFetcherShowcaseTemplate { Ok(Html(
header_props: HeaderProps::default(), EggFetcherShowcaseTemplate {
}) header_props: HeaderProps::default(),
}
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
))
} }

View File

@ -8,7 +8,14 @@ use crate::{
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher, 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 tower_http::trace::TraceLayer;
use tracing::info_span; use tracing::info_span;
@ -16,15 +23,15 @@ pub fn get_router() -> Router {
Router::new() Router::new()
.route("/", get(render_index)) .route("/", get(render_index))
.route("/blog", get(render_blog_post_list)) .route("/blog", get(render_blog_post_list))
.route("/blog/tags/:tag", get(render_blog_post_list)) .route("/blog/tags/{tag}", get(render_blog_post_list))
.route("/blog/:post_id", get(render_blog_post)) .route("/blog/{post_id}", get(render_blog_post))
.route("/broadcasts", get(render_broadcast_post_list)) .route("/broadcasts", get(render_broadcast_post_list))
.route("/broadcasts/tags/:tag", get(render_broadcast_post_list)) .route("/broadcasts/tags/{tag}", get(render_broadcast_post_list))
.route("/broadcasts/:post_id", get(render_blog_post)) .route("/broadcasts/{post_id}", get(render_blog_post))
.route("/contact", get(render_contact)) .route("/contact", get(render_contact))
.route("/showcase", get(render_projects_list)) .route("/showcase", get(render_projects_list))
.route("/showcase/m-logo-svg", get(render_animated_logo)) .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("/portfolio", get(render_portfolio))
.route("/admin", get(render_admin)) .route("/admin", get(render_admin))
.route("/feed.xml", get(render_rss_feed)) .route("/feed.xml", get(render_rss_feed))

View File

@ -335,9 +335,6 @@
.mt-8 { .mt-8 {
margin-top: calc(var(--spacing) * 8); margin-top: calc(var(--spacing) * 8);
} }
.mt-12 {
margin-top: calc(var(--spacing) * 12);
}
.mr-3 { .mr-3 {
margin-right: calc(var(--spacing) * 3); margin-right: calc(var(--spacing) * 3);
} }
@ -936,11 +933,6 @@
margin-block: calc(var(--spacing) * 6); margin-block: calc(var(--spacing) * 6);
} }
} }
.lg\:mt-8 {
@media (width >= 64rem) {
margin-top: calc(var(--spacing) * 8);
}
}
.lg\:mt-10 { .lg\:mt-10 {
@media (width >= 64rem) { @media (width >= 64rem) {
margin-top: calc(var(--spacing) * 10); margin-top: calc(var(--spacing) * 10);

View File

@ -2,8 +2,7 @@
<aside class="flex justify-center items-center pr-3 shrink-0"> <aside class="flex justify-center items-center pr-3 shrink-0">
{% match skill.thumbnail %} {% match skill.thumbnail %}
{% when Some with (source) %} {% when Some with (source) %}
{% let skill_name = skill.name.clone() %} {% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, &format!("{} cover", skill.name), Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, format!("{skill_name} cover"), Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
<figure class="mx-4 my-2"> <figure class="mx-4 my-2">
{{picture|safe}} {{picture|safe}}
</figure> </figure>