Compare commits

..

3 Commits

Author SHA1 Message Date
2979e21285 broadcasts and 404 errors
Some checks failed
test / cargo test (push) Failing after 59s
2024-10-02 15:32:40 +02:00
4f09373df3 copy og tag images 2024-10-02 10:24:24 +02:00
8c72e7b440 fix bug 2024-10-01 21:04:30 +02:00
27 changed files with 309 additions and 67 deletions

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /admin

View File

@ -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,
}
} }

View File

@ -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))
} }

View File

@ -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
//

View File

@ -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(),
}) })

View File

@ -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(),
}),
}) })
} }

View 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(),
})
}

View File

@ -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()
)?; )?;

View File

@ -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;

View 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(),
})
}

View File

@ -0,0 +1 @@
pub mod egg_fetcher;

View 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)
}

View File

@ -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;

View File

@ -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());

View File

@ -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(

View File

@ -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 {

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View 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 %}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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"