Compare commits
3 Commits
49f830cd44
...
2979e21285
Author | SHA1 | Date | |
---|---|---|---|
2979e21285 | |||
4f09373df3 | |||
8c72e7b440 |
@ -28,7 +28,7 @@ This week I've attended a [Rusty game jam #2](https://itch.io/jam/rusty-jam-2).
|
|||||||
|
|
||||||
![Egg fetcher game preview](/images/uploads/screenshot-from-2022-06-26-22-37-16.png "Egg fetcher game preview")
|
![Egg fetcher game preview](/images/uploads/screenshot-from-2022-06-26-22-37-16.png "Egg fetcher game preview")
|
||||||
|
|
||||||
[You can check the result built with WASM here.](/showcase/egg-fetcher/)
|
[You can check the result built with WASM here.](/showcase/egg-fetcher)
|
||||||
|
|
||||||
## What's up with the weeklys
|
## What's up with the weeklys
|
||||||
|
|
||||||
|
3
justfile
3
justfile
@ -30,7 +30,7 @@ test_watch:
|
|||||||
cargo watch -x test
|
cargo watch -x test
|
||||||
|
|
||||||
# Run server in production mode
|
# Run server in production mode
|
||||||
prod $TARGET="PROD":
|
prod $TARGET="PROD" $RUST_LOG="info":
|
||||||
cargo run --release
|
cargo run --release
|
||||||
|
|
||||||
# Wait for port to listen to connections
|
# Wait for port to listen to connections
|
||||||
@ -52,6 +52,7 @@ clean:
|
|||||||
# SSG
|
# SSG
|
||||||
ssg:
|
ssg:
|
||||||
- wget --no-convert-links -r -p -E -P dist --no-host-directories 127.0.0.1:{{port}}
|
- wget --no-convert-links -r -p -E -P dist --no-host-directories 127.0.0.1:{{port}}
|
||||||
|
find generated_images/ -name "*_og*" -exec cp --parents {} dist/ \;
|
||||||
|
|
||||||
# Preview server
|
# Preview server
|
||||||
preview:
|
preview:
|
||||||
|
2
robots.txt
Normal file
2
robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /admin
|
@ -4,13 +4,19 @@ use tracing::debug;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
||||||
post_utils::post_listing::get_post_list,
|
post_utils::{post_listing::get_post_list, post_parser::ParseResult},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_popular_blog_tags() -> Result<Vec<String>, StatusCode> {
|
pub async fn get_popular_tags(segment: Option<String>) -> Result<Vec<String>, StatusCode> {
|
||||||
const TAGS_LENGTH: usize = 7;
|
const TAGS_LENGTH: usize = 7;
|
||||||
|
|
||||||
let post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH).await?;
|
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH).await?;
|
||||||
|
post_list.retain(|post| post.metadata.published);
|
||||||
|
|
||||||
|
if let Some(segment) = segment {
|
||||||
|
post_list.retain(|post| post.metadata.segments.contains(&segment));
|
||||||
|
}
|
||||||
|
|
||||||
let tags_sum = post_list
|
let tags_sum = post_list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|post| post.metadata.tags)
|
.flat_map(|post| post.metadata.tags)
|
||||||
@ -27,12 +33,32 @@ pub async fn get_popular_blog_tags() -> Result<Vec<String>, StatusCode> {
|
|||||||
debug!("Tag: {}, Count: {}", tag, count);
|
debug!("Tag: {}, Count: {}", tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
let popular_blog_tags = sorted_tags_by_count
|
let popular_tags = sorted_tags_by_count
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tag_count| tag_count.0)
|
.map(|tag_count| tag_count.0)
|
||||||
.filter(|tag| tag != "dev")
|
.filter(|tag| tag != "dev")
|
||||||
.take(TAGS_LENGTH)
|
.take(TAGS_LENGTH)
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
Ok(popular_blog_tags)
|
Ok(popular_tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posts_by_tag(
|
||||||
|
post_list: Vec<ParseResult<BlogPostMetadata>>,
|
||||||
|
tag: &Option<String>,
|
||||||
|
) -> Vec<ParseResult<BlogPostMetadata>> {
|
||||||
|
match tag {
|
||||||
|
Some(tag) => post_list
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post| {
|
||||||
|
post.metadata
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.map(|post_tag| post_tag.to_lowercase())
|
||||||
|
.collect::<String>()
|
||||||
|
.contains(&tag.to_lowercase())
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
None => post_list,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,5 +49,5 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
let response = feed_builder.to_string();
|
let response = feed_builder.to_string();
|
||||||
return Ok(([(header::CONTENT_TYPE, "application/xml")], response));
|
Ok(([(header::CONTENT_TYPE, "application/xml")], response))
|
||||||
}
|
}
|
||||||
|
20
src/main.rs
20
src/main.rs
@ -1,4 +1,5 @@
|
|||||||
use axum::{self};
|
use askama_axum::IntoResponse;
|
||||||
|
use axum::{self, extract::OriginalUri, http::StatusCode};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_livereload::LiveReloadLayer;
|
use tower_livereload::LiveReloadLayer;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@ -33,9 +34,14 @@ async fn main() {
|
|||||||
.nest_service("/styles", ServeDir::new("styles"))
|
.nest_service("/styles", ServeDir::new("styles"))
|
||||||
.nest_service("/images", ServeDir::new("static/images"))
|
.nest_service("/images", ServeDir::new("static/images"))
|
||||||
.nest_service("/fonts", ServeDir::new("static/fonts"))
|
.nest_service("/fonts", ServeDir::new("static/fonts"))
|
||||||
|
.nest_service("/files", ServeDir::new("static/files"))
|
||||||
.nest_service("/generated_images", ServeDir::new("generated_images"))
|
.nest_service("/generated_images", ServeDir::new("generated_images"))
|
||||||
|
.nest_service("/egg-fetcher", ServeDir::new("static/egg-fetcher"))
|
||||||
.nest_service("/svg", ServeDir::new("static/svg"))
|
.nest_service("/svg", ServeDir::new("static/svg"))
|
||||||
.nest_service("/config.yml", ServeDir::new("static/resources/config.yml")); // Decap CMS config
|
.nest_service("/config.yml", ServeDir::new("static/resources/config.yml")) // Decap CMS config
|
||||||
|
.nest_service("/robots.txt", ServeDir::new("robots.txt"));
|
||||||
|
|
||||||
|
let app = app.fallback(handler_404);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let app = app.layer(LiveReloadLayer::new());
|
let app = app.layer(LiveReloadLayer::new());
|
||||||
@ -48,6 +54,11 @@ async fn main() {
|
|||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handler_404(OriginalUri(original_uri): OriginalUri) -> impl IntoResponse {
|
||||||
|
info!("{original_uri} not found");
|
||||||
|
(StatusCode::NOT_FOUND, "nothing to see here")
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Socials
|
// TODO Socials
|
||||||
// - fotos
|
// - fotos
|
||||||
// background gradient color
|
// background gradient color
|
||||||
@ -56,3 +67,8 @@ async fn main() {
|
|||||||
// TODO after release
|
// TODO after release
|
||||||
// OG tags
|
// OG tags
|
||||||
// - projects page
|
// - projects page
|
||||||
|
// TODO broken links
|
||||||
|
// showcase/eggfetcher
|
||||||
|
// broadcasts/
|
||||||
|
// manifest.json
|
||||||
|
//
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{OriginalUri, Path, RawQuery},
|
extract::{OriginalUri, Path},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
@ -9,7 +9,7 @@ use tracing::debug;
|
|||||||
use crate::{
|
use crate::{
|
||||||
blog_posts::{
|
blog_posts::{
|
||||||
blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
||||||
tag_list::get_popular_blog_tags,
|
tag_list::{get_popular_tags, get_posts_by_tag},
|
||||||
},
|
},
|
||||||
components::site_header::{HeaderProps, Link},
|
components::site_header::{HeaderProps, Link},
|
||||||
filters,
|
filters,
|
||||||
@ -21,10 +21,11 @@ use crate::{
|
|||||||
#[template(path = "blog_post_list.html")]
|
#[template(path = "blog_post_list.html")]
|
||||||
pub struct PostListTemplate {
|
pub struct PostListTemplate {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub og_title: String,
|
||||||
|
pub segment: String,
|
||||||
pub posts: Vec<ParseResult<BlogPostMetadata>>,
|
pub posts: Vec<ParseResult<BlogPostMetadata>>,
|
||||||
pub tag: Option<String>,
|
|
||||||
pub header_props: HeaderProps,
|
pub header_props: HeaderProps,
|
||||||
pub blog_tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub featured_projects: Vec<ParseResult<ProjectMetadata>>,
|
pub featured_projects: Vec<ParseResult<ProjectMetadata>>,
|
||||||
pub current_url: String,
|
pub current_url: String,
|
||||||
}
|
}
|
||||||
@ -37,30 +38,17 @@ pub async fn render_blog_post_list(
|
|||||||
let tag = tag.map(|Path(tag)| tag);
|
let tag = tag.map(|Path(tag)| tag);
|
||||||
|
|
||||||
let (blog_tags, featured_projects, mut post_list) = try_join!(
|
let (blog_tags, featured_projects, mut post_list) = try_join!(
|
||||||
get_popular_blog_tags(),
|
get_popular_tags(Some("blog".to_string())),
|
||||||
get_featured_projects(),
|
get_featured_projects(),
|
||||||
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
post_list.sort_by_key(|post| post.metadata.date);
|
post_list.sort_by_key(|post| post.metadata.date);
|
||||||
post_list.retain(|post| post.metadata.published);
|
post_list.retain(|post| post.metadata.published);
|
||||||
|
post_list.retain(|post| post.metadata.segments.contains(&"blog".to_string()));
|
||||||
post_list.reverse();
|
post_list.reverse();
|
||||||
|
|
||||||
let posts = match &tag {
|
let posts = get_posts_by_tag(post_list, &tag);
|
||||||
Some(tag) => post_list
|
|
||||||
.into_iter()
|
|
||||||
.filter(|post| {
|
|
||||||
post.metadata
|
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.map(|post_tag| post_tag.to_lowercase())
|
|
||||||
.collect::<String>()
|
|
||||||
.contains(&tag.to_lowercase())
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
None => post_list,
|
|
||||||
};
|
|
||||||
|
|
||||||
let header_props = match tag {
|
let header_props = match tag {
|
||||||
Some(_) => HeaderProps::with_back_link(Link {
|
Some(_) => HeaderProps::with_back_link(Link {
|
||||||
href: "/blog".to_string(),
|
href: "/blog".to_string(),
|
||||||
@ -71,18 +59,19 @@ pub async fn render_blog_post_list(
|
|||||||
|
|
||||||
debug!("uri:{:?}", original_uri);
|
debug!("uri:{:?}", original_uri);
|
||||||
|
|
||||||
let title = if let Some(tag) = &tag {
|
let (title, og_title) = if let Some(tag) = &tag {
|
||||||
format!("{tag} blog posts")
|
(format!("#{tag}"), format!("{tag} blog posts"))
|
||||||
} else {
|
} else {
|
||||||
"Blog posts".to_string()
|
("Blog posts".to_string(), "Blog posts".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PostListTemplate {
|
Ok(PostListTemplate {
|
||||||
title,
|
title,
|
||||||
|
og_title,
|
||||||
|
segment: "blog".to_string(),
|
||||||
posts,
|
posts,
|
||||||
tag,
|
|
||||||
header_props,
|
header_props,
|
||||||
blog_tags,
|
tags: blog_tags,
|
||||||
featured_projects,
|
featured_projects,
|
||||||
current_url: original_uri.to_string(),
|
current_url: original_uri.to_string(),
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use axum::extract::OriginalUri;
|
||||||
use axum::{extract::Path, http::StatusCode};
|
use axum::{extract::Path, http::StatusCode};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
@ -17,15 +18,38 @@ pub struct BlogPostTemplate {
|
|||||||
pub body: String,
|
pub body: String,
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
pub segment: String,
|
||||||
pub header_props: HeaderProps,
|
pub header_props: HeaderProps,
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
pub thumbnail: Option<String>,
|
pub thumbnail: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_blog_post(Path(post_id): Path<String>) -> Result<BlogPostTemplate, StatusCode> {
|
pub async fn render_blog_post(
|
||||||
|
Path(post_id): Path<String>,
|
||||||
|
OriginalUri(original_uri): OriginalUri,
|
||||||
|
) -> Result<BlogPostTemplate, StatusCode> {
|
||||||
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
|
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
|
||||||
let parse_post = parse_post::<BlogPostMetadata>(&path, true);
|
let parse_post = parse_post::<BlogPostMetadata>(&path, true);
|
||||||
let parsed = parse_post.await?;
|
let parsed = parse_post.await?;
|
||||||
|
let segment = if original_uri.to_string().starts_with("/blog") {
|
||||||
|
"blog"
|
||||||
|
} else if original_uri.to_string().starts_with("/broadcasts") {
|
||||||
|
"broadcasts"
|
||||||
|
} else {
|
||||||
|
"blog"
|
||||||
|
};
|
||||||
|
|
||||||
|
let header_props = match segment {
|
||||||
|
"blog" => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/blog".to_string(),
|
||||||
|
label: "All posts".to_string(),
|
||||||
|
}),
|
||||||
|
"broadcasts" => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/broadcasts".to_string(),
|
||||||
|
label: "All broadcasts".to_string(),
|
||||||
|
}),
|
||||||
|
_ => HeaderProps::default(),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(BlogPostTemplate {
|
Ok(BlogPostTemplate {
|
||||||
title: parsed.metadata.title,
|
title: parsed.metadata.title,
|
||||||
@ -33,10 +57,8 @@ pub async fn render_blog_post(Path(post_id): Path<String>) -> Result<BlogPostTem
|
|||||||
tags: parsed.metadata.tags,
|
tags: parsed.metadata.tags,
|
||||||
body: parsed.body,
|
body: parsed.body,
|
||||||
slug: parsed.slug,
|
slug: parsed.slug,
|
||||||
|
segment: segment.to_string(),
|
||||||
thumbnail: parsed.metadata.thumbnail,
|
thumbnail: parsed.metadata.thumbnail,
|
||||||
header_props: HeaderProps::with_back_link(Link {
|
header_props,
|
||||||
href: "/blog".to_string(),
|
|
||||||
label: "All posts".to_string(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
66
src/pages/broadcast_list.rs
Normal file
66
src/pages/broadcast_list.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{OriginalUri, Path},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use tokio::try_join;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blog_posts::{
|
||||||
|
blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
||||||
|
tag_list::{get_popular_tags, get_posts_by_tag},
|
||||||
|
},
|
||||||
|
components::site_header::{HeaderProps, Link},
|
||||||
|
post_utils::post_listing::get_post_list,
|
||||||
|
projects::featured_projects::get_featured_projects,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::blog_post_list::PostListTemplate;
|
||||||
|
|
||||||
|
pub async fn render_broadcast_post_list(
|
||||||
|
tag: Option<Path<String>>,
|
||||||
|
OriginalUri(original_uri): OriginalUri,
|
||||||
|
) -> Result<PostListTemplate, StatusCode> {
|
||||||
|
// 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 (popular_tags, featured_projects, mut post_list) = try_join!(
|
||||||
|
get_popular_tags(Some("broadcasts".to_string())),
|
||||||
|
get_featured_projects(),
|
||||||
|
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
post_list.sort_by_key(|post| post.metadata.date);
|
||||||
|
post_list.retain(|post| post.metadata.published);
|
||||||
|
post_list.retain(|post| post.metadata.segments.contains(&"broadcasts".to_string()));
|
||||||
|
post_list.reverse();
|
||||||
|
|
||||||
|
let posts = get_posts_by_tag(post_list, &tag);
|
||||||
|
|
||||||
|
let header_props = match tag {
|
||||||
|
Some(_) => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/broadcasts".to_string(),
|
||||||
|
label: "All broadcasts".to_string(),
|
||||||
|
}),
|
||||||
|
None => HeaderProps::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("uri:{:?}", original_uri);
|
||||||
|
|
||||||
|
let title = if let Some(tag) = &tag {
|
||||||
|
format!("#{tag} broadcasts")
|
||||||
|
} else {
|
||||||
|
"Broadcasts".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PostListTemplate {
|
||||||
|
title: title.clone(),
|
||||||
|
og_title: title,
|
||||||
|
segment: "broadcasts".to_string(),
|
||||||
|
posts,
|
||||||
|
header_props,
|
||||||
|
tags: popular_tags,
|
||||||
|
featured_projects,
|
||||||
|
current_url: original_uri.to_string(),
|
||||||
|
})
|
||||||
|
}
|
@ -5,7 +5,7 @@ use tokio::try_join;
|
|||||||
use crate::{
|
use crate::{
|
||||||
blog_posts::{
|
blog_posts::{
|
||||||
blog_post_model::BlogPostMetadata, featured_blog_posts::get_featured_blog_posts,
|
blog_post_model::BlogPostMetadata, featured_blog_posts::get_featured_blog_posts,
|
||||||
tag_list::get_popular_blog_tags,
|
tag_list::get_popular_tags,
|
||||||
},
|
},
|
||||||
components::site_header::HeaderProps,
|
components::site_header::HeaderProps,
|
||||||
filters,
|
filters,
|
||||||
@ -24,7 +24,7 @@ pub struct IndexTemplate {
|
|||||||
|
|
||||||
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
||||||
let (blog_tags, featured_blog_posts, featured_projects) = try_join!(
|
let (blog_tags, featured_blog_posts, featured_projects) = try_join!(
|
||||||
get_popular_blog_tags(),
|
get_popular_tags(Some("blog".to_string())),
|
||||||
get_featured_blog_posts(),
|
get_featured_blog_posts(),
|
||||||
get_featured_projects()
|
get_featured_projects()
|
||||||
)?;
|
)?;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod blog_post_list;
|
pub mod blog_post_list;
|
||||||
pub mod blog_post_page;
|
pub mod blog_post_page;
|
||||||
|
pub mod broadcast_list;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod project_list;
|
pub mod project_list;
|
||||||
|
pub mod showcase;
|
||||||
|
16
src/pages/showcase/egg_fetcher.rs
Normal file
16
src/pages/showcase/egg_fetcher.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
|
||||||
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "egg_fetcher_page.html")]
|
||||||
|
pub struct EggFetcherShowcaseTemplate {
|
||||||
|
header_props: HeaderProps,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> {
|
||||||
|
Ok(EggFetcherShowcaseTemplate {
|
||||||
|
header_props: HeaderProps::default(),
|
||||||
|
})
|
||||||
|
}
|
1
src/pages/showcase/mod.rs
Normal file
1
src/pages/showcase/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod egg_fetcher;
|
63
src/picture_generator/image_src_generator.rs
Normal file
63
src/picture_generator/image_src_generator.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use image::ImageReader;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
image_generator::generate_images,
|
||||||
|
picture_markup_generator::{get_export_formats, get_generated_file_name, get_image_path},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn generate_image_with_src(
|
||||||
|
orig_img_path: &str,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
suffix: &str,
|
||||||
|
generate_image: bool,
|
||||||
|
) -> Result<String, anyhow::Error> {
|
||||||
|
let path_to_generated = get_generated_file_name(orig_img_path);
|
||||||
|
let file_stem = path_to_generated.file_stem().unwrap().to_str().unwrap();
|
||||||
|
let path_to_generated = path_to_generated.with_file_name(format!("{file_stem}{suffix}"));
|
||||||
|
|
||||||
|
let disk_img_path =
|
||||||
|
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
||||||
|
let resolutions = [(width, height, 1.)];
|
||||||
|
|
||||||
|
let exported_formats = get_export_formats(orig_img_path);
|
||||||
|
let exported_format = *exported_formats.first().unwrap();
|
||||||
|
|
||||||
|
let path_to_generated_arc = Arc::new(path_to_generated);
|
||||||
|
let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
|
||||||
|
|
||||||
|
if generate_image {
|
||||||
|
rayon::spawn(move || {
|
||||||
|
let orig_img = ImageReader::open(&disk_img_path)
|
||||||
|
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap();
|
||||||
|
let path_to_generated = path_to_generated_clone.as_ref();
|
||||||
|
|
||||||
|
let result = generate_images(
|
||||||
|
&orig_img,
|
||||||
|
path_to_generated,
|
||||||
|
&resolutions,
|
||||||
|
&[exported_format],
|
||||||
|
)
|
||||||
|
.with_context(|| "Failed to generate images".to_string());
|
||||||
|
if let Err(e) = result {
|
||||||
|
tracing::error!("Error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let path_to_generated = Arc::clone(&path_to_generated_arc);
|
||||||
|
|
||||||
|
let image_path = get_image_path(
|
||||||
|
&path_to_generated,
|
||||||
|
resolutions.first().expect("Should this error ever happen?"),
|
||||||
|
&exported_format,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(image_path)
|
||||||
|
}
|
@ -23,5 +23,6 @@ It should be used from the templates as well
|
|||||||
|
|
||||||
pub mod export_format;
|
pub mod export_format;
|
||||||
pub mod image_generator;
|
pub mod image_generator;
|
||||||
|
pub mod image_src_generator;
|
||||||
pub mod picture_markup_generator;
|
pub mod picture_markup_generator;
|
||||||
pub mod resolutions;
|
pub mod resolutions;
|
||||||
|
@ -119,7 +119,7 @@ pub fn generate_picture_markup(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_image_path(path: &Path, resolution: &(u32, u32, f32), format: &ExportFormat) -> String {
|
pub fn get_image_path(path: &Path, resolution: &(u32, u32, f32), format: &ExportFormat) -> String {
|
||||||
let path_name = path.to_str().expect("Image has to have a valid path");
|
let path_name = path.to_str().expect("Image has to have a valid path");
|
||||||
let (width, height, _) = resolution;
|
let (width, height, _) = resolution;
|
||||||
let extension = format.get_extension();
|
let extension = format.get_extension();
|
||||||
@ -197,7 +197,7 @@ fn strip_prefixes(path: &Path) -> &Path {
|
|||||||
parent_path
|
parent_path
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
|
pub fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
|
||||||
let path = Path::new(&orig_img_path);
|
let path = Path::new(&orig_img_path);
|
||||||
// let parent = path
|
// let parent = path
|
||||||
// .parent()
|
// .parent()
|
||||||
@ -247,7 +247,7 @@ fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32,
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
|
pub fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
|
||||||
let path = Path::new(&orig_img_path)
|
let path = Path::new(&orig_img_path)
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str());
|
.and_then(|ext| ext.to_str());
|
||||||
|
@ -2,8 +2,9 @@ use crate::{
|
|||||||
feed::render_rss_feed,
|
feed::render_rss_feed,
|
||||||
pages::{
|
pages::{
|
||||||
admin::render_admin, blog_post_list::render_blog_post_list,
|
admin::render_admin, blog_post_list::render_blog_post_list,
|
||||||
blog_post_page::render_blog_post, contact::render_contact, index::render_index,
|
blog_post_page::render_blog_post, broadcast_list::render_broadcast_post_list,
|
||||||
project_list::render_projects_list,
|
contact::render_contact, index::render_index, project_list::render_projects_list,
|
||||||
|
showcase::egg_fetcher::render_egg_fetcher,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
||||||
@ -16,8 +17,12 @@ pub fn get_router() -> Router {
|
|||||||
.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/tags/:tag", get(render_broadcast_post_list))
|
||||||
|
.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/:project_slug", get(render_egg_fetcher))
|
||||||
.route("/admin", get(render_admin))
|
.route("/admin", get(render_admin))
|
||||||
.route("/feed.xml", get(render_rss_feed))
|
.route("/feed.xml", get(render_rss_feed))
|
||||||
.layer(
|
.layer(
|
||||||
|
@ -2125,6 +2125,11 @@ article a:visited {
|
|||||||
.xl\:grid-cols-\[1fr_2fr\] {
|
.xl\:grid-cols-\[1fr_2fr\] {
|
||||||
grid-template-columns: 1fr 2fr;
|
grid-template-columns: 1fr 2fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xl\:gap-x-32 {
|
||||||
|
-moz-column-gap: 8rem;
|
||||||
|
column-gap: 8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
@ -26,8 +26,6 @@
|
|||||||
<!-- Tailwind output file -->
|
<!-- Tailwind output file -->
|
||||||
<link rel="stylesheet" href="/styles/output.css" />
|
<link rel="stylesheet" href="/styles/output.css" />
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/images/m-logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/m-logo.svg" />
|
||||||
<link rel="icon" type="image/png" href="/images/m-logo-192.png" />
|
<link rel="icon" type="image/png" href="/images/m-logo-192.png" />
|
||||||
</head>
|
</head>
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
{% block og_meta %}
|
{% block og_meta %}
|
||||||
<meta property="og:title" content="{{title}}" />
|
<meta property="og:title" content="{{title}}" />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:url" content="https://michalvanko.dev/blog/{{slug}}" />
|
<meta property="og:url" content="https://michalvanko.dev/{{segment}}/{{slug}}" />
|
||||||
{% match thumbnail %}
|
{% match thumbnail %}
|
||||||
{% when Some with (img) %}
|
{% when Some with (img) %}
|
||||||
<meta property="og:image" content="https://michalvanko.dev{{img}}" />
|
{% let src = crate::picture_generator::image_src_generator::generate_image_with_src(img, 1200, 630, "_og", true).unwrap_or("thumbnail not found".to_string())|safe %}
|
||||||
|
<meta property="og:image" content="https://michalvanko.dev{{src}}" />
|
||||||
{% when None %}
|
{% when None %}
|
||||||
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
|
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block og_meta %}
|
{% block og_meta %}
|
||||||
<meta property="og:title" content="{{title}} @michalvankodev" />
|
<meta property="og:title" content="{{og_title}} @michalvankodev" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://michalvanko.dev{{current_url}}" />
|
<meta property="og:url" content="https://michalvanko.dev{{current_url}}" />
|
||||||
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
|
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
|
||||||
@ -10,24 +10,20 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<section id="blog-container" class="lg:grid lg:grid-cols-[2fr_1fr] lg:grid-rows-[min-content_1fr] lg:gap-x-32 max-w-maxindex mx-auto">
|
<section id="blog-container" class="lg:grid lg:grid-cols-[2fr_1fr] lg:grid-rows-[min-content_1fr] xl:gap-x-32 max-w-maxindex mx-auto">
|
||||||
<section id="blog-list" class="lg:row-span-2">
|
<section id="blog-list" class="lg:row-span-2">
|
||||||
{% if posts.len() == 0 %}
|
{% if posts.len() == 0 %}
|
||||||
<p class="no-posts">You've found void in the space.</p>
|
<p class="no-posts">You've found void in the space.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h1 class="m-5 text-4xl text-blue-950 font-extrabold md:text-6xl">
|
<h1 class="m-5 text-4xl text-blue-950 font-extrabold md:text-6xl">
|
||||||
{% if let Some(t) = tag %}
|
{{title}}
|
||||||
#{{t}}
|
|
||||||
{% else %}
|
|
||||||
Blog posts
|
|
||||||
{% endif %}
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<section id="blog-tags">
|
<section id="blog-tags">
|
||||||
<ul class="mx-5">
|
<ul class="mx-5">
|
||||||
{% for tag in blog_tags %}
|
{% for tag in tags %}
|
||||||
<li class="inline-block mx-0.5 p-0.5 md:text-xl">
|
<li class="inline-block mx-0.5 p-0.5 md:text-xl">
|
||||||
<a href="/blog/tags/{{tag}}" class="text-pink-950">#{{tag|capitalize}}</a>
|
<a href="/{{segment}}/tags/{{tag}}" class="text-pink-950">#{{tag|capitalize}}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<header>
|
<header>
|
||||||
<h3 class="text-lg font-bold mb-1 md:text-3xl">
|
<h3 class="text-lg font-bold mb-1 md:text-3xl">
|
||||||
<a rel="prefetch" href="/blog/{{post.slug}}" class="text-blue-950 visited:text-purple-700 no-underline">{{post.metadata.title}}</a>
|
<a rel="prefetch" href="/{{segment}}/{{post.slug}}" class="text-blue-950 visited:text-purple-700 no-underline">{{post.metadata.title}}</a>
|
||||||
</h3>
|
</h3>
|
||||||
</header>
|
</header>
|
||||||
<section class="text-base leading-5 text-slate-800 md:text-xl text-justify">{{post.body|description_filter|safe}}</section>
|
<section class="text-base leading-5 text-slate-800 md:text-xl text-justify">{{post.body|description_filter|safe}}</section>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<ul class="inline-block">
|
<ul class="inline-block">
|
||||||
{% for tag in post.metadata.tags %}
|
{% for tag in post.metadata.tags %}
|
||||||
<li class="inline-block">
|
<li class="inline-block">
|
||||||
<a href="/blog/tags/{{tag}}" class="text-pink-950 no-underline">#{{tag|capitalize}}</a>
|
<a href="/{{segment}}/tags/{{tag}}" class="text-pink-950 no-underline">#{{tag|capitalize}}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
16
templates/egg_fetcher_page.html
Normal file
16
templates/egg_fetcher_page.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Egg fetcher showcase{% endblock %}
|
||||||
|
|
||||||
|
{% block og_meta %}
|
||||||
|
<meta property="og:title" content="Egg fetcher showcase" />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:url" content="https://michalvanko.dev/showcase/egg-fetcher" />
|
||||||
|
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="article-body">
|
||||||
|
{% include "showcase/egg-fetcher.html" %}
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
@ -41,12 +41,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="blog" class="lg:col-span-2 lg:row-start-2 xl:col-auto xl:row-start-auto xl:row-span-2">
|
<section id="blog" class="lg:col-span-2 lg:row-start-2 xl:col-auto xl:row-start-auto xl:row-span-2">
|
||||||
|
{% let segment = "blog".to_string() %}
|
||||||
<h2 class="text-blue-950 font-bold text-2xl md:text-4xl m-5"><a href="/blog" class="text-blue-950 no-underline">Blog</a></h2>
|
<h2 class="text-blue-950 font-bold text-2xl md:text-4xl m-5"><a href="/blog" class="text-blue-950 no-underline">Blog</a></h2>
|
||||||
<section id="blog-tags">
|
<section id="blog-tags">
|
||||||
<ul class="mx-5">
|
<ul class="mx-5">
|
||||||
{% for tag in blog_tags %}
|
{% for tag in blog_tags %}
|
||||||
<li class="inline-block mx-0.5 p-0.5 md:text-xl">
|
<li class="inline-block mx-0.5 p-0.5 md:text-xl">
|
||||||
<a href="/blog/tags/{{tag}}" class="text-pink-950">#{{tag|capitalize}}</a>
|
<a href="/{{segment}}/tags/{{tag}}" class="text-pink-950">#{{tag|capitalize}}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<ul class="inline">
|
<ul class="inline">
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<li class="inline italic text-blue-700 md:text-lg">
|
<li class="inline italic text-blue-700 md:text-lg">
|
||||||
<a href="/blog/tags/{{tag}}">#{{tag}}</a>
|
<a href="/{{segment}}/tags/{{tag}}">#{{tag}}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
15
templates/showcase/egg-fetcher.html
Normal file
15
templates/showcase/egg-fetcher.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<h1>Egg-fetcher</h1>
|
||||||
|
|
||||||
|
<p>As mentioned in <a href="/blog/2022-06-26-our-attempt-at-rusty-game-jam-weekly-25-2022">Weekly #25-2022</a>, I've attended the <a href="https://itch.io/jam/rusty-jam-2">Rusty game jam #2</a> where we had a week to <strong>create a game with Rust</strong>.</p>
|
||||||
|
<p>I've teamed up with <a href="https://github.com/silen-z/">@silen-z</a>. We haven't been able to finish the game. We didn't even make the mechanics we were thinking of. But I'd like to show <strong>the incomplete version of Egg-fetcher</strong> anyway. As we built it with Rust and <a href="https://bevyengine.org/">bevy engine</a> we were able to <a href="https://github.com/septum/rusty_jam_bevy_template">reuse a template</a> that had a configured WASM build. Therefore is very easy to just present the game in the browser.</p>
|
||||||
|
<iframe title="Egg fetcher game" src="/egg-fetcher/index.html" width="800" height="600"></iframe>
|
||||||
|
<p><strong>The only functional controls are arrows</strong>. We have built a collision system where the chickens should move out of the way of the player and his pet. The player is not able to move through the fences and so on. We wanted to <strong>create a puzzle</strong> where you would have to play fetch with your pet dog to control the chickens and simultaneously control player movement.</p>
|
||||||
|
<p>This was only my 3rd attempt at a Rust codebase and therefore I got pretty much always stuck at some problem with borrow-checker or lifetimes.
|
||||||
|
I learned many things and I'd like to continue with Rust and use it more in my side-projects.</p>
|
||||||
|
<style>
|
||||||
|
iframe {
|
||||||
|
height: 720px;
|
||||||
|
width: 1280px !important;
|
||||||
|
max-width: 1280px !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -25,17 +25,17 @@
|
|||||||
rel="license noopener noreferrer"
|
rel="license noopener noreferrer"
|
||||||
style="display: inline-block"
|
style="display: inline-block"
|
||||||
>CC BY-NC-ND 4.0<img
|
>CC BY-NC-ND 4.0<img
|
||||||
src="images/creative-commons/cc.svg"
|
src="/images/creative-commons/cc.svg"
|
||||||
alt="cc"
|
alt="cc"
|
||||||
class="inline-block h-6 mx-0.5"
|
class="inline-block h-6 mx-0.5"
|
||||||
height="24"
|
height="24"
|
||||||
width="24" /><img
|
width="24" /><img
|
||||||
src="images/creative-commons/by.svg"
|
src="/images/creative-commons/by.svg"
|
||||||
alt="by"
|
alt="by"
|
||||||
class="inline-block h-6 mx-0.5"
|
class="inline-block h-6 mx-0.5"
|
||||||
height="24"
|
height="24"
|
||||||
width="24" /><img
|
width="24" /><img
|
||||||
src="images/creative-commons/nc.svg"
|
src="/images/creative-commons/nc.svg"
|
||||||
alt="nc"
|
alt="nc"
|
||||||
class="inline-block h-6 mx-0.5"
|
class="inline-block h-6 mx-0.5"
|
||||||
height="24"
|
height="24"
|
||||||
|
Loading…
Reference in New Issue
Block a user