2 Commits

Author SHA1 Message Date
michalvankodev 5a1fb0c5f9 added logo bg pulsing effect
test / cargo test (push) Failing after 1m24s
2025-03-13 22:40:21 +01:00
michalvankodev f09ce128e8 add mastodon
test / cargo test (push) Failing after 1m41s
2024-12-05 11:19:04 +01:00
54 changed files with 2623 additions and 2694 deletions
+3 -3
View File
@@ -13,9 +13,9 @@ jobs:
name: release
runs-on: ubuntu-latest
container:
image: node:24
image: node:22
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: cache
uses: Swatinem/rust-cache@v2
@@ -47,7 +47,7 @@ jobs:
working-directory: axum_server
- name: Server log
run: cat axum_server/server.log
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v4
with:
name: dist
path: axum_server/dist/
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
name: cargo test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cd axum_server && cargo test --all-features
-4
View File
@@ -23,7 +23,3 @@ Cargo.lock
# Image generator
generated_images/
node_modules/
package-lock.json
.aider*
-59
View File
@@ -1,59 +0,0 @@
---
description: Create a new blog post with today's date and proper front matter. Takes a title/topic as argument.
---
Create a new blog post file for this project.
**Input:** $@ - The title/topic for the blog post
---
## Instructions
1. Generate a URL-friendly slug from the title:
- Lowercase
- Replace spaces and special characters with hyphens
- Remove consecutive hyphens
- Strip leading/trailing hyphens
2. Create the file at `_posts/blog/YYYY-MM-DD-{slug}.md` where YYYY-MM-DD is today's date
3. Use this front matter template:
```yaml
---
layout: blog
title: {Original Title}
segments:
- blog
published: false
date: {YYYY-MM-DD}T12:00:00.000Z
thumbnail:
tags:
- News
---
## {First section heading}
{Leave space for content}
```
4. **Important:** The date in the filename (YYYY-MM-DD) MUST match the date in the front matter's `date` field
5. Confirm the file was created with the full path
## Available Segments
- `blog` (default)
- `featured`
- `broadcasts`
- `cookbook`
## Common Tags
- News
- Development
- Programming
- Rust
- JavaScript
- TypeScript
- Keyboards
- Tutorial
- Personal
-98
View File
@@ -1,98 +0,0 @@
---
name: review-article
description: Review blog posts for authenticity, structure, clarity, and improvements while preserving the author's unique voice. Provides suggestions only - no edits.
---
# Article Review Assistant
You are a thoughtful editorial reviewer who helps the author improve their blog posts while preserving their authentic voice and personal style.
## Author's Voice Profile
Based on analysis of previous writing, the author's style includes:
- **Personal & conversational tone** - Uses first person ("I", "I've"), shares personal experiences and stories
- **Authentic storytelling** - Often starts with personal context before diving into technical content
- **Strategic emphasis** - Uses **bold** for key concepts and _italics_ for subtle emphasis
- **Practical examples** - Includes code snippets, real-world scenarios, actionable takeaways
- **Honest vulnerability** - Shares struggles, mistakes, and learning moments openly
- **Structured flow** - Uses headers, lists, blockquotes, and code blocks effectively
- **Conversational closings** - Often ends with summaries, personal reflections, or invites reader engagement
## Review Process
**Input:** Path to a markdown blog post file.
1. **Read the article** - Understand the full content, message, and intent
2. **Analyze against voice profile** - Check for authenticity and consistency
3. **Identify improvement opportunities** - Structure, clarity, emphasis, spelling
4. **Provide actionable suggestions** - Specific, not vague
---
## Review Output Format
### 📋 Article Summary
Brief 1-2 sentence summary of what the article is about and its main message.
### ✅ Strengths
What's working well in this article (authentic moments, great explanations, effective structure).
### 🎯 Topic & Story Fit
- Does the topic align with the author's typical subjects (development, personal projects, tutorials, tech insights)?
- Does the narrative flow naturally?
- Any disconnects between the opening story and the main content?
### 📝 Structure Suggestions
Specific recommendations for:
- Header hierarchy and organization
- Paragraph flow and transitions
- Section ordering or grouping
- Opening hook effectiveness
- Closing impact
### 💡 Emphasis Opportunities
Suggest specific places where **bold** or _italics_ could strengthen the message:
- Key concepts that deserve highlighting
- Important takeaways readers should remember
- Subtle points that could use gentle emphasis
### 🔍 Spelling & Grammar
List any typos, grammatical issues, or awkward phrasings found (quote the exact text).
### 🎨 Clarity Improvements
- Sentences or paragraphs that could be clearer
- Technical terms that need brief explanation
- Jargon that might alienate readers
- Places where an example would help
### ⚠️ Authenticity Check
- Any moments that feel inauthentic or forced?
- Does the voice stay consistent throughout?
- Are there places where the author's personality could shine through more?
### 📌 Suggested Improvements (Without Changing Core Concept)
Numbered list of specific, actionable suggestions that:
- Enhance clarity without altering meaning
- Strengthen the narrative without changing the story
- Improve flow without losing the personal touch
---
## Important Guidelines
- **NEVER make edits** - Only provide suggestions
- **Preserve the author's voice** - Don't suggest changes that would make it sound generic
- **Be specific** - Quote exact text when suggesting changes
- **Explain why** - Give reasoning for each suggestion
- **Respect the core message** - Never suggest changes that alter the main concept or story
- **Celebrate authenticity** - Highlight moments where the author's genuine voice shines
- **Consider the audience** - The blog appears to target developers and tech-curious readers
## What NOT to Suggest
- Removing personal anecdotes or stories
- Making the tone more "professional" or formal
- Generic corporate language
- Changes that would alter the fundamental message
- Rewriting in a different voice
+19 -16
View File
@@ -1,26 +1,29 @@
layout {
default_tab_template {
children
pane size=2 borderless=true {
plugin location="zellij:status-bar"
}
pane size=1 borderless=true {
plugin location="zellij:tab-bar"
pane split_direction="vertical" focus=true {
pane edit="src/main.rs"
pane split_direction="horizontal" size=60 {
just { args "server_dev"; }
just { args "test"; }
}
}
pane_template name="just" {
command "just"
start_suspended true
}
tab split_direction="vertical" focus=true {
pane {
just { args "server_dev"; }
just { args "test"; }
floating_panes {
pane {
command "just"
args "tailwind"
}
pane {
just { args "tailwind"; }
just { args "decap_server"; }
pane {
command "just"
args "decap_server"
}
}
pane size=2 borderless=true {
plugin location="zellij:status-bar"
}
pane size=1 borderless=true {
plugin location="zellij:tab-bar"
}
tab
}
-193
View File
@@ -1,193 +0,0 @@
# AGENTS.md
## Project Overview
Personal website and blog hosted at https://michalvanko.dev. A static site generator (SSG) built with Rust using the Axum web framework. During development, content is served via HTTP with SSR; for production, the site is exported to static HTML files using wget crawling.
**Technology Stack:**
- **Backend:** Rust, Axum web framework, Tokio async runtime
- **Templating:** Askama (compile-time template engine)
- **Styling:** Tailwind CSS v4
- **Content:** Markdown with YAML front matter (gray_matter), parsed with pulldown-cmark
- **CMS:** Decap CMS (formerly Netlify CMS) for content management
- **Build Tool:** Just (command runner, similar to make)
- **Deployment:** Caddy reverse proxy, rsync to remote server
## Architecture
### Directory Structure
```
├── src/ # Rust source code (~40 files, ~2090 lines)
│ ├── main.rs # Entry point, sets up Axum server with static file serving
│ ├── router.rs # Route definitions and HTTP layer
│ ├── pages/ # Page handlers (index, blog, portfolio, etc.)
│ ├── blog_posts/ # Blog post model and metadata
│ ├── projects/ # Project model and featured projects
│ ├── components/ # Reusable UI components (site_header)
│ ├── filters/ # Askama template filters (markdown, date, truncate)
│ ├── post_utils/ # Post parsing, listing, segments, tags
│ ├── picture_generator/ # Responsive image generation (multiple sizes/formats)
│ └── feed.rs # RSS feed generation
├── templates/ # Askama HTML templates
│ ├── base.html # Base template with head, header, footer
│ ├── components/ # Reusable template partials
│ ├── sections/ # Page section templates
│ └── icons/ # SVG icon sprite
├── styles/
│ ├── input.css # Tailwind source with custom theme
│ └── output.css # Generated CSS (gitignored)
├── static/ # Static assets served directly
│ ├── images/ # Site images
│ ├── fonts/ # Custom web fonts (Baloo2)
│ ├── svg/ # SVG icons
│ └── resources/ # Decap CMS config
├── _posts/blog/ # Blog posts (Markdown with front matter)
├── _projects/ # Showcase projects (Markdown with front matter)
├── _pages/ # Static pages (portfolio.md)
├── generated_images/ # Auto-generated responsive images (gitignored)
├── dist/ # SSG output folder (gitignored)
└── target/ # Rust build artifacts (gitignored)
```
### Key Entry Points
- `src/main.rs` - Server startup, static file routing, livereload (debug only)
- `src/router.rs` - All route definitions, maps URLs to page handlers
- `src/pages/index.rs` - Homepage, demonstrates async data loading pattern
### Data Flow
1. **Content Loading:** Markdown files in `_posts/`, `_projects/`, `_pages/` are parsed at runtime
2. **Front Matter:** YAML metadata extracted via `gray_matter` crate
3. **Template Rendering:** Askama templates receive structs with data
4. **Image Generation:** Images auto-generated in multiple sizes on first request
5. **SSG Export:** `wget` crawls running server, saves HTML to `dist/`
## Code Conventions
### Rust Patterns
- **Module Organization:** Each domain has a `mod.rs` with submodules
- **Async Handlers:** Page handlers use `async fn` returning `Result<impl IntoResponse, StatusCode>`
- **Template Structs:** Each page has a corresponding `#[derive(Template)]` struct
- **Error Handling:** Uses `anyhow` for errors, `StatusCode` for HTTP responses
- **Parallel Loading:** `tokio::try_join!` for concurrent data fetching
### Template Patterns
- **Inheritance:** Templates extend `base.html` using `{% block content %}`
- **Includes:** Reusable partials via `{% include "component.html" %}`
- **Filters:** Custom filters in `src/filters/` (e.g., `{{ content|markdown }}`)
- **Configuration:** `askama.toml` sets template directory and whitespace handling
### Naming Conventions
- **Files:** snake_case for Rust files, kebab-case for templates
- **Routes:** kebab-case URLs (`/blog`, `/showcase/m-logo-svg`)
- **Front Matter:** snake_case fields in YAML
### Configuration Files
| File | Purpose |
|------|---------|
| `Cargo.toml` | Rust dependencies, package metadata |
| `askama.toml` | Template directory config |
| `.prettierrc` | JS/JSON formatting (trailing commas, 2-space, single quotes) |
| `.nvmrc` | Node.js version: `lts/*` |
| `.npmrc` | npm config: `engine-strict=true` |
| `justfile` | Build commands and deployment scripts |
| `renovate.json` | Dependency update automation |
| `static/resources/config.yml` | Decap CMS configuration |
## Setup Requirements
### Prerequisites
- **Rust:** Stable toolchain (uses edition 2021)
- **Node.js:** LTS version (for Tailwind CSS, Decap CMS)
- **Just:** Command runner (`cargo install just` or system package)
- **cargo-watch:** For development hot reload (`cargo install cargo-watch`)
### Environment Variables
- `PORT` - Server port (default: 3080)
- `RUST_LOG` - Logging level (default: `axum_server=debug,tower_http=debug`)
- `TARGET` - Build target mode (used in `just prod`)
### Development Setup
```bash
# Install dependencies
npm install
# Run development server (starts all services in parallel)
just dev
# This runs: server_dev, tailwind watch, decap_server
# Or run individually:
just server_dev # Rust server with hot reload
just tailwind # CSS watch mode
just decap_server # Local CMS backend
```
### Common Commands
```bash
just test # Run Rust tests
just test_watch # Run tests with watch mode
just prod # Run server in release mode
just export # Generate static site to dist/
just deploy # rsync dist/ to remote server
```
## Important Files
### Configuration
- `Cargo.toml` - Rust dependencies (axum, askama, pulldown-cmark, etc.)
- `justfile` - All build/dev/deploy commands
- `static/resources/config.yml` - Decap CMS collections and fields
- `styles/input.css` - Tailwind theme customization (colors, fonts, spacing)
### CI/CD
- `.gitea/workflows/test.yaml` - Runs `cargo test` on push/PR
- `.gitea/workflows/release.yaml` - Builds release, runs SSG export, uploads `dist/` artifact
### Content Structure
- `_posts/blog/` - Blog posts with YAML front matter (title, segments, published, date, thumbnail, tags)
- `_projects/` - Showcase projects (title, displayed, cover_image, link, classification, tags, featured)
- `_pages/portfolio.md` - Portfolio page content (work history, education)
### Key Dependencies
**Rust:**
- `axum` - Web framework
- `askama` - Compile-time templates
- `pulldown-cmark` - Markdown parsing
- `gray_matter` - YAML front matter extraction
- `tokio` - Async runtime
- `tower-http` - HTTP middleware (tracing, static files)
- `tower-livereload` - Development hot reload
- `image` - Image processing for responsive images
- `syntect` - Syntax highlighting
**Node.js:**
- `tailwindcss` v4 - CSS framework
- `@tailwindcss/cli` - Tailwind CLI
## Notes for Agents
1. **Template Changes:** Askama templates are compiled into Rust code. After template changes, the project recompiles automatically with `cargo-watch`.
2. **Adding Routes:** Add handler in `src/pages/`, register in `src/router.rs`, create template in `templates/`.
3. **Content Model:** All content uses YAML front matter. See `static/resources/config.yml` for field definitions.
4. **Image Handling:** Images are auto-generated in multiple sizes. The `picture_generator` module creates responsive `<picture>` elements.
5. **Debug vs Release:** Debug builds include livereload. Release builds are optimized for production.
6. **SSG Process:** The static site is generated by running the server and crawling with `wget`. All linked content must be discoverable from the homepage.
+28 -10
View File
@@ -6,26 +6,44 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
askama = { version = "0.15" }
axum = "0.8.0"
askama = { version = "0.12", features = ["with-axum", "mime", "mime_guess"] }
askama_axum = "0.4.0"
axum = "0.7.3"
chrono = { version = "0.4.31", features = ["serde"] }
pulldown-cmark = { version = "0.13" }
gray_matter = "0.3.0"
pulldown-cmark = { version = "0.12" }
gray_matter = "0.2.6"
rss = "2.0.7"
serde = "1.0.195"
serde_json = "1.0.111"
tokio = { version = "1.35.1", features = ["full"] }
tower-http = { version = "0.6.0", features = ["trace", "fs"] }
tower-livereload = "0.10.0"
tower-livereload = "0.9.2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
image = "0.25.6"
image = "0.25.2"
anyhow = "1.0.86"
rayon = "1.10.0"
syntect = { version = "5.3.0", default-features = false, features = ["default-fancy"] }
syntect = "5.2.0"
indoc = "2.0.5"
askama_escape = "0.15.0"
askama_escape = "0.10.3"
mime_guess = "2.0.5"
[dev-dependencies]
pretty_assertions = "1"
[build]
rustflags = ["-Z", "threads=8"]
# [target.x86_64-unknown-linux-gnu]
# rustflags = [
# "-C", "link-arg=-fuse-ld=lld"
# ]
[profile.dev]
debug = true
opt-level = 0
# codegen-units = 16
# lto = "thin"
panic = "unwind"
strip = false
incremental = true
[profile.dev.package.askama_derive]
opt-level = 3
@@ -3,6 +3,7 @@ layout: blog
title: Error handling with Either<Type>
segments:
- blog
- featured
published: true
date: 2022-02-28T11:30:54.195Z
tags:
@@ -1,59 +0,0 @@
---
layout: blog
title: Somewhere in the sky
segments:
- blog
published: true
date: 2025-06-11T15:50:45.527Z
thumbnail: /images/uploads/dscf4399.jpg
tags:
- News
- Personal
- Travel
---
## Realization
What can you do on an airplane when you cannot move as your son is sleeping on your lap? You can **write a new blog post**, "obviously". Why can't I find more time to write more posts? I lack the motivation, to be honest. Now that anyone can just ask an LLM to spit out an original blog post on demand, who will search for original, honest content written by a human?
I've set expectations for my wife that this year we won't be going on a vacation, and she has overruled and found a one-week trip to Mallorca. I can confirm that we will be **coming back to Mallorca** for further exploration. This week was very nice. I haven't made any commitments on what I'm going to do with my career. I've already **accomplished** that since this year, **I will work on Rust projects**.
## Drift
In my latest vlog, I mentioned that I should talk more about burning out, and that I don't feel like I am burning out. Since then, I stopped streaming and stopped producing videos. **Did I jinx myself?** I don't know. Being active on social media is not something for me. I enjoy streaming, but I just had to put my family above my fulfillment. **I enjoy putting my son to sleep** much more. He is growing so fast. I expect that in a few years **we will start streaming together** as he explores the world of computers.
I expect to find **more time for streaming** when the sun falls in earlier hours.
You should've seen him. He was able to fill his bucket and pour it into a hole in the sand more than 100 times in a row. Where he finds the energy to do this is unbelievable. He is such an extraordinary individual.
## Inertia
*What about the game progress?*
**The game is in a terrible state**. I realized that the **whole animation system has to be rewritten** as it is unreliable. And that's almost everything that we have been working on. I've started to **write the game design document** so I don't lose my thoughts on mechanics and game setting. I should find more time to code as well, but currently, **I have just enough work and studies to keep me occupied**.
## Sight
Here are some photos from the trip:
![My beautiful family](/images/uploads/dscf4379.jpg "My beautiful fam")
![Chasing dreams](/images/uploads/img_20250607_202440.jpg "Chasing dreams")
![The sun for my moon](/images/uploads/dscf4333.jpg "The sun for my moon")
![Stomp](/images/uploads/dscf4274.jpg "Stomp")
![Lil bird](/images/uploads/dscf4399.jpg "Lil bird")
![Fuji X-Michal with Viltrox AF 23/1.4 XF](/images/uploads/img_20250610_204627.jpg "Fuji X-Michal Viltrox AF 23/1.4 XF")
Some of them are taken with my *Fuji X-Michal* and I have to say I've fallen in love with the machine. I wish some features were more accessible, and I hope with the new Fuji X-E5 announcement that the *Film simulation recipes* will be included in a future firmware upgrade. The current configuration is very misleading and not user-friendly.
I've also ordered a new Sigma 16-300, hoping it will be delivered before the vacation. I have already **been waiting for it for 2 months**, and still no indication of when it will arrive. I am pretty new to the hobby, and it looks like when ordering a new gear, there is a **guarantee of a long waiting time**, or it is out of stock pretty quickly.
I'd like to **post more pictures** here on the blog. I'm planning on releasing albums. I don't know if I should also make some changes to the visualization, or if the current blog page rendering system is fine as it is. **Let me know if you have some cool ideas**.
I'm going to keep it short. We've landed successfully. The vacation was amazing. We are definitely coming back to Mallorca. Expect more content (soon).
Happy summer.
@@ -1,91 +0,0 @@
---
layout: blog
title: Week with my-pi-agent
segments:
- blog
- featured
published: true
date: 2026-04-01T12:00:00.000Z
thumbnail: /images/uploads/pi-logo.svg
tags:
- News
- AI
- Agents
- Development
---
Last week I've set a goal for myself to go 100% without writing a single line of code manually. I've been trying agentic workflows for a year now. My first agentic tool I've tried was [aider](https://aider.chat/) and it was pretty cool experience at that time. For the last few months I've been using [OpenCode](https://opencode.ai/) but I haven't been able to let it control 100% of my results. Mainly because it hasn't been able to earn my trust. I've been watching different tools for a long time and decided that it was time to try out something minimal. Something that I can make work for me. Make it work with my workflows and tools I use during work.
## Meet Pi
<!-- ![Pi agent logo](/images/uploads/pi-logo.svg 'Pi agent logo') -->
<figure>
<img src="/images/uploads/pi-logo.svg" alt="Pi agent logo" class="max-w-[280px] h-[280px] bg-black">
</figure>
[Pi](https://pi.dev) is a **bare bones CLI agent** similar to Claude Code, Codex CLI and OpenCode.
Pi itself is very simple. It might be a turnoff for anyone who wants to just download and start using it. You have to spend some time tinkering it.
There is a lot of [packages](https://pi.dev/packages), that can bring the functionality of other CLIs to Pi.
The community around Pi is very fresh and thriving. We are still in the early adopters era of agentic programming. I'd consider every **package as a potential threat** and **cautiously review** them before installing.
I discovered Pi when [OpenClaw](https://openclaw.ai/) was released. I was curious about what is powering _OpenClaw_. Back then I wasn't really interested in Pi.
But after few weeks with [OpenCode](https://opencode.ai/), I wasn't feeling like the tool is working with my workflow.
After trying out Pi I got hooked immediately when I prompted it to generate a custom plugin that is customized to my workflow and environment.
I use a nerdish Desktop environment:
- [Fedora Linux](https://fedoraproject.org/)
- [Noctalia shell](https://noctalia.dev/)
- [Niri Window manager](https://niri-wm.github.io/niri/) (special kind of tiling window manager)
- [fish](https://fishshell.com/) as a shell
- [ZelliJ](https://zellij.dev/) as a terminal multiplexer
I don't know anyone who uses the same tools I do.
Other agentic CLIs are like taxi cars, you hop-in, you can tell the driver where you want to go. But it is out of your hands how you get there. You are often restricted in the capabilities of the CLI.
The features you leverage right now can be pulled out whenever the author of the CLI consider them not useful. With Pi you can **inspect every single line of code**. Why, and what gets executed and when.
With Pi, you build your own ride. Do you need to just get some mail from post office? You build a bicycle. Do you need a team of X "engineers", feel free to burn your tokens however you like - build an airplane.
Want to go the to moon? Build yourself a rocketship. There are no limits.
With no other CLI agent I was ever **motivated to create my own plugins, commands, workflows**. I was always trying to just search for stuff that "just works", but it never worked for me.
First plugin I've created was that I wanted to have a [`/commit` command](https://github.com/michalvankodev/my-pi-agent/blob/main/agent/extensions/commit.ts).
- This command should not do `git commit` for me.
- It should just generate a commit message and open an editor in a new _ZelliJ_ pane.
- I can not only review but edit if I am not satisfied with the output. I can even cancel the commit if I opt to.
One, two prompts and voilà - I have my first custom plugin. Nothing ground-breaking but it is working just how I like it.
![My Pi says Hi!](/images/uploads/pi-screenshot.png 'My Pi says Hi!')
## What works for me
In my experience a simple approach turned out to have the best results considering getting stuff done, cheaper and reliable.
I've tried to set up interactive sub-agents that talk between each other and I was stunned for a moment.
After few minutes the output of 4 agents working together was very little and I almost ran out of available tokens.
Then I continued with just one main agent and it was able to finish whole task much quicker.
The main workflow for me (for backend tasks) is:
1. Prompt agent with what are we going to do. "Gather information about existing code". Write a specification file and split work into tasks.
We are going to update the specification after each task gets done.
2. [`/clarify`](https://github.com/michalvankodev/my-pi-agent/blob/main/agent/prompts/clarify.md) This is the **most powerful prompt** in my workflow. It asks agent to ask clarifying questions. This prevents the agent to invent wheels that already exists or rail off from what we want to accomplish.
3. Start working on tasks one by one
When the context-window gets too big, there is a decision to make. With `/tree` command you can traverse your conversation to a point (after the 2nd step for example) where the agent has enough context about all the tasks that you can continue without starting with fresh context window. Or you can create a `/new` session and prompt it to gather information about the tasks with the link to the spec.
`/tree` is very useful feature. I use it to fork my current session into multiple new ones and you can parallelize your work (I don't recommend it every time).
Two other features I use constantly:
- Steering - When you see the agent doing something you want to immediately fix, you steer it. Your message
arrives as soon as the current tool call finishes.
- Follow up - When you want to tell the agent something or request additional work, you follow up. That
message gets queued after the agent completes all pending work, instead of waiting for your next prompt.
There's still more to learn. This is just the experience I've had with Pi during the last week.
For front-end task I'd choose a different workflow. I'd incorporate `agent-browser` into the loop. Having constant feedback loop is very powerful and allows the agent to run for longer time (do more without constant supervision)
Another pleasant use experience I had was with **debugging memory issues**. This is a task that if you don't do very often during development, but from time to time it happens that unexpected errors happen due to different resource availability on production systems. I've said to the agent what happens and what I think was the issue (I was way way off).
It created multiple testing scenario applications in `/tmp` directory to just test off hypothesis.
He dug out every single call and investigated deep hierarchy of code deep down to every single dependency in the chain.
We haven't been able to find the issue there. So I've used [`heaptrack`](https://github.com/KDE/heaptrack) to provide actual information about the memory allocations and then I just passed the output to the agent. It just came out with the actual issue and provided fix in few seconds.
It is able to create `bash` commands and execute them in a loop until it discovers significant information. It does so at such velocity that I have to admit I **can't imagine now working without it**. Once you experience this kind of boost **it's unthinkable to go back**.
+1 -1
View File
@@ -2,7 +2,7 @@ port := env_var_or_default('PORT', '3080')
# Tailwind in watch mode
tailwind:
npx @tailwindcss/cli -i ./styles/input.css -o ./styles/output.css --watch
npx tailwindcss -i ./styles/input.css -o ./styles/output.css --watch
# svg sprite creation
svgstore:
-6
View File
@@ -1,6 +0,0 @@
{
"dependencies": {
"@tailwindcss/cli": "^4.1.10",
"tailwindcss": "^4.1.10"
}
}
+4 -13
View File
@@ -1,4 +1,3 @@
use askama::Values;
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use chrono::Utc;
@@ -8,14 +7,6 @@ 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<impl IntoResponse, StatusCode> {
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
.await
@@ -36,14 +27,14 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
.title(Some(post.metadata.title))
.link(Some(format!("https://michalvanko.dev/blog/{}", post.slug)))
.description({
let truncated = truncate_md(&post.body, &EmptyValues, 2)
.unwrap_or("Can't parse post body".to_string());
let parsed_md = parse_markdown(&truncated, &EmptyValues)
let truncated =
truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string());
let parsed_md = parse_markdown(&truncated)
.unwrap_or("Can't process truncated post body".to_string());
Some(parsed_md)
})
.content({
let parsed_md = parse_markdown(&post.body, &EmptyValues)
let parsed_md = parse_markdown(&post.body)
.unwrap_or("Can't process full post body".to_string());
Some(parsed_md)
})
+4 -31
View File
@@ -1,4 +1,3 @@
use core::fmt;
use std::path::Path;
use image::image_dimensions;
@@ -8,8 +7,7 @@ use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing
use tracing::{debug, error};
use crate::picture_generator::{
picture_markup_generator::{generate_picture_markup, should_swap_dimensions},
resolutions::get_max_resolution,
picture_markup_generator::generate_picture_markup, resolutions::get_max_resolution,
};
pub const MAX_BLOG_IMAGE_RESOLUTION: (u32, u32) = (1280, 860);
@@ -21,10 +19,7 @@ enum TextKind {
}
// pub fn parse_markdown(markdown: &str) -> ::askama::Result<String>
pub fn parse_markdown<T: fmt::Display>(
markdown: T,
_: &dyn askama::Values,
) -> ::askama::Result<String> {
pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
@@ -39,8 +34,7 @@ pub fn parse_markdown<T: fmt::Display>(
let theme = theme_set.themes.get("InspiredGitHub").unwrap();
let mut heading_ended: Option<bool> = None;
let mds = markdown.to_string();
let parser = Parser::new_ext(&mds, options).map(|event| match event {
let parser = Parser::new_ext(markdown, options).map(|event| match event {
/*
Parsing images considers `alt` attribute as inner `Text` event
Therefore the `[alt]` is rendered in html as subtitle
@@ -64,31 +58,10 @@ pub fn parse_markdown<T: fmt::Display>(
);
}
// Handle SVG files - don't try to get dimensions (image crate doesn't support SVG)
if dest_url.to_lowercase().ends_with(".svg") {
return Event::Html(
formatdoc!(
r#"<figure>
<img src="{dest_url}" alt="{title}">
<figcaption>
"#
)
.into(),
);
}
let dev_only_img_path =
Path::new("static/").join(dest_url.strip_prefix("/").unwrap_or(&dest_url));
let img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
// We need to take the exif rotation into consideration here
let img_dimensions = {
let orig_img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
if should_swap_dimensions(&dev_only_img_path) {
(orig_img_dimensions.1, orig_img_dimensions.0)
} else {
orig_img_dimensions
}
};
let (max_width, max_height) = get_max_resolution(
img_dimensions,
MAX_BLOG_IMAGE_RESOLUTION.0,
+1 -1
View File
@@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
// This filter does not have extra arguments
pub fn pretty_date(date_time: &DateTime<Utc>, _: &dyn askama::Values) -> ::askama::Result<String> {
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
let formatted = format!("{}", date_time.format("%e %B %Y"));
Ok(formatted)
}
+1 -1
View File
@@ -2,7 +2,7 @@
const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"];
pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result<String> {
pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result<String> {
let description = body
.lines()
.filter(|line| {
+2 -10
View File
@@ -1,17 +1,9 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
#[derive(Template)]
#[template(path = "admin.html")]
pub struct AdminPageTemplate {}
pub async fn render_admin() -> Result<impl IntoResponse, StatusCode> {
Ok(Html(
AdminPageTemplate {}
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
))
pub async fn render_admin() -> AdminPageTemplate {
AdminPageTemplate {}
}
+3 -6
View File
@@ -1,13 +1,10 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::http::StatusCode;
#[derive(Template)]
#[template(path = "assets/animated_logo.html")]
pub struct AnimatedLogoTemplate {}
pub async fn render_animated_logo() -> Result<impl IntoResponse, StatusCode> {
Ok(Html(AnimatedLogoTemplate {}.render().unwrap()))
pub async fn render_animated_logo() -> Result<AnimatedLogoTemplate, StatusCode> {
Ok(AnimatedLogoTemplate {})
}
+11 -17
View File
@@ -1,8 +1,6 @@
use askama::Template;
use axum::{
extract::{OriginalUri, Path},
http::StatusCode,
response::{Html, IntoResponse},
};
use tokio::try_join;
use tracing::debug;
@@ -24,7 +22,7 @@ use super::post_list::PostListTemplate;
pub async fn render_blog_post_list(
tag: Option<Path<String>>,
OriginalUri(original_uri): OriginalUri,
) -> Result<impl IntoResponse, StatusCode> {
) -> 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);
@@ -52,18 +50,14 @@ pub async fn render_blog_post_list(
("Blog posts".to_string(), "Blog posts".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(),
))
Ok(PostListTemplate {
title,
og_title,
segment: Segment::Blog,
posts,
header_props,
tags: blog_tags,
featured_projects,
current_url: original_uri.to_string(),
})
}
+12 -17
View File
@@ -1,6 +1,5 @@
use askama::Template;
use axum::extract::OriginalUri;
use axum::response::{Html, IntoResponse};
use axum::{extract::Path, http::StatusCode};
use chrono::{DateTime, Utc};
@@ -31,7 +30,7 @@ pub struct BlogPostTemplate {
pub async fn render_blog_post(
Path(post_id): Path<String>,
OriginalUri(original_uri): OriginalUri,
) -> Result<impl IntoResponse, StatusCode> {
) -> Result<BlogPostTemplate, StatusCode> {
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
let post = parse_post::<BlogPostMetadata>(&path).await?;
let segment = if original_uri.to_string().starts_with("/blog") {
@@ -60,21 +59,17 @@ pub async fn render_blog_post(
_ => HeaderProps::default(),
};
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(),
))
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,
})
}
async fn get_recommended_posts(
+11 -17
View File
@@ -1,8 +1,6 @@
use askama::Template;
use axum::{
extract::{OriginalUri, Path},
http::StatusCode,
response::{Html, IntoResponse},
};
use tokio::try_join;
use tracing::debug;
@@ -23,7 +21,7 @@ use super::post_list::PostListTemplate;
pub async fn render_broadcast_post_list(
tag: Option<Path<String>>,
OriginalUri(original_uri): OriginalUri,
) -> Result<impl IntoResponse, StatusCode> {
) -> 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);
@@ -52,18 +50,14 @@ pub async fn render_broadcast_post_list(
"Broadcasts".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(),
))
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(),
})
}
+7 -14
View File
@@ -1,8 +1,5 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::http::StatusCode;
use crate::components::site_header::HeaderProps;
@@ -21,7 +18,7 @@ pub struct ContactPageTemplate {
pub links: Vec<ContactLink>,
}
pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
let links = vec![
ContactLink {
href: "mailto:michalvankosk@gmail.com".to_string(),
@@ -79,13 +76,9 @@ pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
},
];
Ok(Html(
ContactPageTemplate {
title: "Contact".to_owned(),
header_props: HeaderProps::default(),
links,
}
.render()
.unwrap(),
))
Ok(ContactPageTemplate {
title: "Contact".to_owned(),
header_props: HeaderProps::default(),
links,
})
}
+10 -17
View File
@@ -1,10 +1,7 @@
use std::rc::Rc;
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::http::StatusCode;
use tokio::try_join;
use crate::{
@@ -29,7 +26,7 @@ pub struct IndexTemplate {
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
}
pub async fn render_index() -> Result<impl IntoResponse, StatusCode> {
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!(
get_popular_tags(Some(Segment::Blog)),
get_popular_tags(Some(Segment::Broadcasts)),
@@ -47,16 +44,12 @@ pub async fn render_index() -> Result<impl IntoResponse, StatusCode> {
let featured_broadcasts =
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]);
Ok(Html(
IndexTemplate {
header_props: HeaderProps::default(),
blog_tags,
broadcasts_tags,
featured_blog_posts,
featured_projects,
featured_broadcasts,
}
.render()
.unwrap(),
))
Ok(IndexTemplate {
header_props: HeaderProps::default(),
blog_tags,
broadcasts_tags,
featured_blog_posts,
featured_projects,
featured_broadcasts,
})
}
+6 -14
View File
@@ -1,9 +1,5 @@
use askama::Template;
use axum::{
extract::OriginalUri,
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::{extract::OriginalUri, http::StatusCode};
use tracing::info;
use crate::components::site_header::HeaderProps;
@@ -17,17 +13,13 @@ pub struct NotFoundPage {
pub async fn render_not_found(
OriginalUri(original_uri): OriginalUri,
) -> Result<(StatusCode, impl IntoResponse), StatusCode> {
) -> Result<(StatusCode, NotFoundPage), StatusCode> {
info!("{original_uri} not found");
Ok((
StatusCode::NOT_FOUND,
Html(
NotFoundPage {
title: "This page does not exists".to_owned(),
header_props: HeaderProps::default(),
}
.render()
.unwrap(),
),
NotFoundPage {
title: "This page does not exists".to_owned(),
header_props: HeaderProps::default(),
},
))
}
+12 -19
View File
@@ -1,8 +1,5 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::http::StatusCode;
use serde::Deserialize;
use crate::{
@@ -53,7 +50,7 @@ pub struct PortfolioTemplate {
pub technology_list: Vec<String>,
}
pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> {
pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?;
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
@@ -129,18 +126,14 @@ pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> {
.map(|str| str.to_owned())
.collect();
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(),
))
Ok(PortfolioTemplate {
title: "Portfolio".to_owned(),
body: portfolio.body,
header_props: HeaderProps::default(),
project_list,
workplace_list,
education_list,
contact_links,
technology_list,
})
}
+7 -14
View File
@@ -1,8 +1,5 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::http::StatusCode;
use crate::{
components::site_header::HeaderProps,
@@ -19,20 +16,16 @@ pub struct ProjectListTemplate {
pub header_props: HeaderProps,
}
pub async fn render_projects_list() -> Result<impl IntoResponse, StatusCode> {
pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> {
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
project_list.sort_by_key(|post| post.slug.to_string());
project_list.retain(|project| project.metadata.displayed);
project_list.reverse();
Ok(Html(
ProjectListTemplate {
title: "Showcase".to_owned(),
header_props: HeaderProps::default(),
project_list,
}
.render()
.unwrap(),
))
Ok(ProjectListTemplate {
title: "Showcase".to_owned(),
header_props: HeaderProps::default(),
project_list,
})
}
+5 -12
View File
@@ -1,8 +1,5 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
use axum::http::StatusCode;
use crate::components::site_header::HeaderProps;
@@ -12,12 +9,8 @@ pub struct EggFetcherShowcaseTemplate {
header_props: HeaderProps,
}
pub async fn render_egg_fetcher() -> Result<impl IntoResponse, StatusCode> {
Ok(Html(
EggFetcherShowcaseTemplate {
header_props: HeaderProps::default(),
}
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
))
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> {
Ok(EggFetcherShowcaseTemplate {
header_props: HeaderProps::default(),
})
}
+10 -1
View File
@@ -1,4 +1,5 @@
#[allow(dead_code)]
use image::ImageFormat;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ExportFormat {
Jpeg,
@@ -24,4 +25,12 @@ impl ExportFormat {
ExportFormat::Png => "image/png",
}
}
pub fn get_image_format(&self) -> ImageFormat {
match self {
ExportFormat::Jpeg => ImageFormat::Jpeg,
ExportFormat::Avif => ImageFormat::Avif,
ExportFormat::Svg => ImageFormat::Jpeg, // TODO what now?
ExportFormat::Png => ImageFormat::Png,
}
}
}
+6 -52
View File
@@ -1,14 +1,6 @@
use std::{
fs::{create_dir_all, File},
io::BufWriter,
path::Path,
};
use std::{fs::create_dir_all, path::Path};
use image::{
codecs::{jpeg::JpegEncoder, png::PngEncoder},
imageops::FilterType,
DynamicImage,
};
use image::{imageops::FilterType, DynamicImage};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{debug, error};
@@ -16,7 +8,6 @@ use super::export_format::ExportFormat;
pub fn generate_images(
image: &DynamicImage,
disk_image_path: &Path,
path_to_generated: &Path,
resolutions: &[(u32, u32, f32)],
formats: &[ExportFormat],
@@ -24,6 +15,7 @@ pub fn generate_images(
formats.par_iter().for_each(|format| {
resolutions.par_iter().for_each(|resolution| {
let (width, height, _) = *resolution;
// let image = image.clone();
let resized = image.resize_to_fill(width, height, FilterType::Triangle);
let file_name = path_to_generated.file_name().unwrap().to_str().unwrap();
let save_path = Path::new("./")
@@ -41,32 +33,13 @@ pub fn generate_images(
create_dir_all(parent_dir).unwrap();
}
let encode = {
let buffered_file_write = &mut BufWriter::new(File::create(&save_path).unwrap()); // always seekable
match format {
ExportFormat::Jpeg => {
let encoder = JpegEncoder::new_with_quality(buffered_file_write, 87);
resized.write_with_encoder(encoder)
}
ExportFormat::Avif => todo!(),
ExportFormat::Svg => todo!(),
ExportFormat::Png => {
let encoder = PngEncoder::new_with_quality(
buffered_file_write,
image::codecs::png::CompressionType::Best,
image::codecs::png::FilterType::Adaptive,
);
resized.write_with_encoder(encoder)
}
}
};
match encode {
let result = resized.save_with_format(&save_path, format.get_image_format());
match result {
Err(err) => {
error!("Failed to generate {:?} - {:?}", &save_path, err);
}
Ok(_) => {
_ => {
debug!("Generated image {:?}", &save_path);
let _ = copy_exif(disk_image_path, &save_path);
}
}
});
@@ -74,22 +47,3 @@ pub fn generate_images(
Ok(())
}
pub fn copy_exif(orig_path: &Path, save_path: &Path) -> Result<(), anyhow::Error> {
let status = std::process::Command::new("exiftool")
.args([
"-TagsFromFile",
orig_path.to_str().expect("Orig path should exist"),
"-exif:all",
"-overwrite_original",
save_path.to_str().expect("Save path of image should exist"),
])
.status()?;
if status.success() {
debug!("EXIF copied successfully.");
} else {
error!("Failed to copy EXIF.");
}
Ok(())
}
+4 -6
View File
@@ -8,15 +8,13 @@ use super::{
picture_markup_generator::{get_export_formats, get_generated_file_name, get_image_path},
};
/// Used directly in templates
pub fn generate_image_with_src(
orig_img_path: &str,
width: u32,
height: u32,
suffix: &str,
) -> Result<String, anyhow::Error> {
let orig_path = Path::new(orig_img_path);
let path_to_generated = get_generated_file_name(orig_path);
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}"));
@@ -24,7 +22,7 @@ pub fn generate_image_with_src(
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_path);
let exported_formats = get_export_formats(orig_img_path);
if exported_formats.is_empty() {
return Ok(orig_img_path.to_string());
@@ -41,11 +39,11 @@ pub fn generate_image_with_src(
.unwrap()
.decode()
.unwrap();
let path_to_generated = path_to_generated_clone.as_ref();
let result = generate_images(
&orig_img,
&disk_img_path,
path_to_generated_clone.as_ref(),
path_to_generated,
&resolutions,
&[exported_format],
)
@@ -4,7 +4,7 @@ use std::{
};
use anyhow::Context;
use image::{image_dimensions, ImageDecoder, ImageReader};
use image::{image_dimensions, ImageReader};
use indoc::formatdoc;
use super::{
@@ -14,7 +14,6 @@ use super::{
pub const PIXEL_DENSITIES: [f32; 5] = [1., 1.5, 2., 3., 4.];
/// Used by markdown generator
pub fn generate_picture_markup(
orig_img_path: &str,
width: u32,
@@ -22,29 +21,13 @@ pub fn generate_picture_markup(
alt_text: &str,
class_name: Option<&str>,
) -> Result<String, anyhow::Error> {
let orig_path = Path::new(orig_img_path);
let exported_formats = get_export_formats(orig_img_path);
let class_attr = if let Some(class) = class_name {
format!(r#"class="{class}""#)
} else {
"".to_string()
};
// Handle SVG files - return simple img tag with provided dimensions for display sizing
if orig_img_path.to_lowercase().ends_with(".svg") {
return Ok(formatdoc!(
r#"<img
src="{orig_img_path}"
width="{width}"
height="{height}"
{class_attr}
alt="{alt_text}"
>"#
));
}
let exported_formats = get_export_formats(orig_path);
// Here the resolution is already correct
if exported_formats.is_empty() {
return Ok(formatdoc!(
r#"<img
@@ -56,19 +39,14 @@ pub fn generate_picture_markup(
>"#
));
}
let path_to_generated = get_generated_file_name(orig_path);
let path_to_generated = get_generated_file_name(orig_img_path);
let disk_img_path =
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
// Here we have a problem. The resolution is swapped but we want to generate images with original dimensions which are not here.
let orig_img_dimensions = image_dimensions(&disk_img_path)?;
let resolutions = get_resolutions(
orig_img_dimensions,
width,
height,
should_swap_dimensions(&disk_img_path),
);
let resolutions = get_resolutions(orig_img_dimensions, width, height);
let path_to_generated_arc = Arc::new(path_to_generated);
let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
let resolutions_arc = Arc::new(resolutions);
@@ -82,15 +60,12 @@ pub fn generate_picture_markup(
.unwrap()
.decode()
.unwrap();
let path_to_generated = path_to_generated_clone.as_ref();
let resolutions = resolutions_clone.as_ref();
let exported_formats = exported_formats_clone.as_ref();
let result = generate_images(
&orig_img,
&disk_img_path,
&path_to_generated_clone,
&resolutions_clone,
&exported_formats_clone,
)
.with_context(|| "Failed to generate images".to_string());
let result = generate_images(&orig_img, path_to_generated, resolutions, exported_formats)
.with_context(|| "Failed to generate images".to_string());
if let Err(e) = result {
tracing::error!("Error: {}", e);
}
@@ -148,19 +123,11 @@ pub fn get_image_path(path: &Path, resolution: &(u32, u32, f32), format: &Export
format!("{path_name}_{width}x{height}.{extension}")
}
/// Take original resolution of photo and
/// exif data is not taken into consideration therefore we don't need to do anything here regarding to swapping width-height
fn get_resolutions(
(orig_width, orig_height): (u32, u32),
width: u32,
height: u32,
swap_dimensions: bool,
) -> Vec<(u32, u32, f32)> {
let (width, height) = if swap_dimensions {
(height, width)
} else {
(width, height)
};
let mut resolutions: Vec<(u32, u32, f32)> = vec![];
for pixel_density in PIXEL_DENSITIES {
let (density_width, density_height) = (
@@ -185,22 +152,22 @@ fn get_resolutions(
#[test]
fn test_get_resolutions() {
assert_eq!(
get_resolutions((320, 200), 320, 200, false),
get_resolutions((320, 200), 320, 200),
vec![(320, 200, 1.)],
"Only original size fits"
);
assert_eq!(
get_resolutions((500, 400), 320, 200, false),
get_resolutions((500, 400), 320, 200),
vec![(320, 200, 1.), (480, 300, 1.5), (500, 312, 2.)],
"Should only create sizes that fits and fill the max possible for the last one - width"
);
assert_eq!(
get_resolutions((400, 600), 300, 200, false),
get_resolutions((400, 600), 300, 200),
vec![(300, 200, 1.), (400, 266, 1.5)],
"Should only create sizes that fits and fill the max possible for the last one - height"
);
assert_eq!(
get_resolutions((1200, 900), 320, 200, false),
get_resolutions((1200, 900), 320, 200),
vec![
(320, 200, 1.),
(480, 300, 1.5),
@@ -210,7 +177,6 @@ fn test_get_resolutions() {
],
"Should create all possible sizes, with the last one maxed"
);
// TODO add test for swapping
}
fn strip_prefixes(path: &Path) -> &Path {
@@ -227,9 +193,15 @@ fn strip_prefixes(path: &Path) -> &Path {
parent_path
}
pub fn get_generated_file_name(orig_img_path: &Path) -> PathBuf {
let parent = strip_prefixes(orig_img_path);
let file_name = orig_img_path
pub fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
let path = Path::new(&orig_img_path);
// let parent = path
// .parent()
// .expect("There should be a parent route to an image")
// .strip_prefix(".")
// .unwrap();
let parent = strip_prefixes(path);
let file_name = path
.file_stem()
.expect("There should be a name for every img");
let result = Path::new("/generated_images/")
@@ -244,7 +216,7 @@ pub fn get_generated_file_name(orig_img_path: &Path) -> PathBuf {
#[test]
fn test_get_generated_paths() {
let orig_img_path = Path::new("/images/uploads/img_name.jpg");
let orig_img_path = "/images/uploads/img_name.jpg";
assert_eq!(
get_generated_file_name(orig_img_path)
.to_str()
@@ -261,9 +233,9 @@ fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32,
let (width, height, density) = resolution;
let path_name = path.to_str().expect("Path to an image has to be valid");
let formatted_density = if density.fract() == 0.0 {
format!("{density}") // Convert to integer if there is no decimal part
format!("{}", density) // Convert to integer if there is no decimal part
} else {
format!("{density:.1}") // Format to 1 decimal place if there is a fractional part
format!("{:.1}", density) // Format to 1 decimal place if there is a fractional part
};
format!("{path_name}_{width}x{height}.{extension} {formatted_density}x")
})
@@ -271,8 +243,10 @@ fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32,
.join(", ")
}
pub fn get_export_formats(orig_img_path: &Path) -> Vec<ExportFormat> {
let path = orig_img_path.extension().and_then(|ext| ext.to_str());
pub fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
let path = Path::new(&orig_img_path)
.extension()
.and_then(|ext| ext.to_str());
match path {
// THINK: Do we want to enable avif? It's very expensive to encode
@@ -284,29 +258,11 @@ pub fn get_export_formats(orig_img_path: &Path) -> Vec<ExportFormat> {
}
}
pub fn should_swap_dimensions(img_path: &Path) -> bool {
let orientation = ImageReader::open(img_path)
.unwrap()
.into_decoder()
.unwrap()
.orientation()
.unwrap();
matches!(
orientation,
image::metadata::Orientation::Rotate90
| image::metadata::Orientation::Rotate270
| image::metadata::Orientation::Rotate90FlipH
| image::metadata::Orientation::Rotate270FlipH
)
}
#[test]
fn test_get_export_formats() {
assert_eq!(
get_export_formats(Path::new("/images/uploads/img_name.jpg")),
// vec![ExportFormat::Avif, ExportFormat::Jpeg]
vec![ExportFormat::Jpeg]
get_export_formats("/images/uploads/img_name.jpg"),
vec![ExportFormat::Avif, ExportFormat::Jpeg]
)
}
#[test]
@@ -336,21 +292,24 @@ fn test_generate_picture_markup() {
let height = 200;
let orig_img_path = "/images/uploads/2020-03-23_20-24-06_393.jpg";
let result = indoc! {
r#"<picture>
<source
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x"
type="image/jpeg"
>
<img
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
width="300"
height="200"
alt="Testing image alt"
>
</picture>"#,
r#"<picture>
<source
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.avif 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.avif 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.avif 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.avif 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.avif 4x"
type="image/avif"
>
<source
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x"
type="image/jpeg"
>
<img
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
width="300"
height="200"
alt="Testing image alt"
>
</picture>"#,
};
pretty_assertions::assert_eq!(
assert_eq!(
generate_picture_markup(orig_img_path, width, height, "Testing image alt", None,)
.expect("picture markup has to be generated"),
result
+6 -13
View File
@@ -8,14 +8,7 @@ use crate::{
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher,
},
};
use askama::Template;
use axum::{
extract::MatchedPath,
http::{Request, StatusCode},
response::IntoResponse,
routing::get,
Router,
};
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
use tower_http::trace::TraceLayer;
use tracing::info_span;
@@ -23,15 +16,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))
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
<!-- P shape: outer boundary clockwise, inner hole counter-clockwise -->
<path fill="#fff" fill-rule="evenodd" d="
M165.29 165.29
H517.36
V400
H400
V517.36
H282.65
V634.72
H165.29
Z
M282.65 282.65
V400
H400
V282.65
Z
"/>
<!-- i dot -->
<path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

+55 -107
View File
@@ -1,113 +1,63 @@
@import "tailwindcss";
@theme {
--font-sans: "Baloo2", "Baloo2 Noto Fallback", "Baloo2 Fallback", "ui-sans-serif", "system-ui", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--text-readxl: 1.75rem;
--text-readxl--line-height: 2.25rem;
--text-readxl--letter-spacing: -0.015rem;
--text-readxl--font-weight: 400;
--spacing-note: 60rem;
--spacing-read: 64rem;
--spacing-image: min(70rem, 95vw);
--spacing-maxindex: 100rem;
--color-blue-50: #f1f7fe;
--color-blue-100: #e1effd;
--color-blue-200: #bddefa;
--color-blue-300: #82c3f7;
--color-blue-400: #42a6f0;
--color-blue-500: #1789e0;
--color-blue-600: #0a6cbf;
--color-blue-700: #0a569a;
--color-blue-800: #0c4980;
--color-blue-900: #103e6a;
--color-blue-950: #0b2746;
--color-pink-50: #fff4fd;
--color-pink-100: #ffe7fb;
--color-pink-200: #ffcff7;
--color-pink-300: #fea6eb;
--color-pink-400: #fc76dd;
--color-pink-500: #f342ca;
--color-pink-600: #d722a9;
--color-pink-700: #b31889;
--color-pink-800: #92166e;
--color-pink-900: #771859;
--color-pink-950: #500238;
--color-purple-50: #F8F5FC;
--color-purple-100: #D5C2ED;
--color-purple-200: #B28EDE;
--color-purple-300: #8F5BCF;
--color-purple-400: #6D30B9;
--color-purple-500: #5F2AA2;
--color-purple-600: #52248A;
--color-purple-700: #441E73;
--color-purple-800: #36185C;
--color-purple-900: #281244;
--color-purple-950: #1A0C2D;
}
@font-face {
font-family: 'Baloo2';
font-style: normal;
font-display: swap;
src:
local('Baloo2'),
url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2');
}
/* latin-ext */
@font-face {
font-family: 'Baloo 2';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Baloo2 Fallback';
font-style: normal;
font-weight: 400;
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
local('Arial');
ascent-override: 111.2%;
descent-override: 54.05%;
line-gap-override: 0%;
size-adjust: 96.95%;
}
@font-face {
font-family: 'Baloo2 Noto Fallback';
font-style: normal;
font-weight: 400;
src: local('Noto Sans');
ascent-override: 88%;
descent-override: none;
line-gap-override: 0%;
size-adjust: 92%;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
a {
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
@font-face {
font-family: 'Baloo2';
font-style: normal;
font-display: swap;
src:
local('Baloo2'),
url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2');
}
/* latin-ext */
@font-face {
font-family: 'Baloo 2';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
strong {
@apply font-medium;
@font-face {
font-family: 'Baloo2 Fallback';
font-style: normal;
font-weight: 400;
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
local('Arial');
ascent-override: 111.2%;
descent-override: 54.05%;
line-gap-override: 0%;
size-adjust: 96.95%;
}
@font-face {
font-family: 'Baloo2 Noto Fallback';
font-style: normal;
font-weight: 400;
src: local('Noto Sans');
ascent-override: 88%;
descent-override: none;
line-gap-override: 0%;
size-adjust: 92%;
}
}
a {
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
}
strong {
@apply font-medium;
}
.article-body {
h1 {
@apply px-4 text-2xl font-semibold text-blue-900 mb-3 mt-4 max-w-read mx-auto md:text-4xl lg:text-5xl;
}
h2 {
@apply px-4 text-xl font-semibold text-blue-900 mb-3 mt-4 max-w-read mx-auto md:text-2xl md:mb-6 md:mt-8 lg:mb-8 lg:mt-12 lg:text-4xl;
}
@@ -132,16 +82,15 @@
@apply p-4;
img {
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
@apply rounded shadow-md mx-auto lg:max-w-image;
}
}
figcaption {
@apply mt-2 text-center text-sm italic text-blue-800 md:text-base lg:text-lg;
}
table {
@apply text-sm mx-auto my-4 max-w-image table-auto border-collapse border-spacing-12 border border-slate-200 rounded-sm md:text-base lg:text-xl lg:my-8;
@apply text-sm mx-auto my-4 max-w-image table-auto border-collapse border-spacing-12 border border-slate-200 rounded md:text-base lg:text-xl lg:my-8;
}
thead {
@@ -170,11 +119,11 @@
}
:not(pre) code {
@apply text-pink-900 rounded-sm border border-blue-300 px-1 py-0.5 bg-blue-100 text-sm md:text-base lg:text-xl;
@apply text-pink-900 rounded border border-blue-300 px-1 py-0.5 bg-blue-100 text-sm md:text-base lg:text-xl;
}
pre code pre {
@apply mx-2 rounded-sm lg:mx-auto lg:text-lg shadow-xs lg:max-w-note;
@apply mx-2 rounded lg:mx-auto lg:text-lg shadow-sm lg:max-w-note;
}
ul,
@@ -189,13 +138,12 @@
ul {
@apply list-disc;
}
ol {
@apply list-decimal;
}
iframe {
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
@apply rounded shadow-md mx-auto lg:max-w-image;
}
}
@@ -332,4 +280,4 @@ article a {
/* animation-duration: 5.5s; */
/* transition: transform 5.4s ease-in-out; */
/* opacity: 1; */
/* } */
/* } */
+2185 -1631
View File
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./templates/**/**.html"],
theme: {
extend: {
fontFamily: {
sans: [
"Baloo2",
"Baloo2 Noto Fallback",
"Baloo2 Fallback",
"ui-sans-serif",
"system-ui",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
],
},
spacing: {
note: "60rem",
read: "64rem",
image: "min(70rem, 95vw)",
maxindex: "100rem",
},
width: {
note: "60rem",
read: "64rem",
image: "min(70rem, 95vw)",
maxindex: "100rem",
},
fontSize: {
readxl: [
"1.75rem",
{
lineHeight: "2.25rem",
letterSpacing: "-0.015em",
fontWeight: "400",
},
],
},
colors: {
// blue: {
// 50: "#ecf6fe",
// 100: "#d9edfc",
// 200: "#b3dbf9",
// 300: "#8ecaf6",
// 400: "#68b8f3",
// 500: "#42a6f0",
// 600: "#3585c0",
// 700: "#286490",
// 800: "#1F4E71",
// 900: "#173A54",
// 950: "#0F2637",
// },
blue: {
50: "#f1f7fe",
100: "#e1effd",
200: "#bddefa",
300: "#82c3f7",
400: "#42a6f0",
500: "#1789e0",
600: "#0a6cbf",
700: "#0a569a",
800: "#0c4980",
900: "#103e6a",
950: "#0b2746",
},
// pink: {
// 50: "#FFFBFE",
// 100: "#FFE4F9",
// 200: "#FECEF4",
// 300: "#FEB8EF",
// 400: "#fea6eb",
// 500: "#D38AC3",
// 600: "#B476A7",
// 700: "#96628B",
// 800: "#774E6E",
// 900: "#593A52",
// 950: "#3A2636",
// },
pink: {
50: "#fff4fd",
100: "#ffe7fb",
200: "#ffcff7",
300: "#fea6eb",
400: "#fc76dd",
500: "#f342ca",
600: "#d722a9",
700: "#b31889",
800: "#92166e",
900: "#771859",
950: "#500238",
},
purple: {
50: "#F8F5FC",
100: "#D5C2ED",
200: "#B28EDE",
300: "#8F5BCF",
400: "#6D30B9",
500: "#5F2AA2",
600: "#52248A",
700: "#441E73",
800: "#36185C",
900: "#281244",
950: "#1A0C2D",
},
},
},
},
plugins: [],
};
+1 -1
View File
@@ -41,7 +41,7 @@
<section id="recommended-articles">
<hr class="border-slate-300 m-5 md:my-8">
<h2 class="m-5 mt-8 text-2xl md:text-2xl lg:text-4xl lg:mt-10 text-blue-900 lg:mb-10 font-bold">Further reading</h2>
<h2 class="m-5 text-2xl md:text-2xl lg:text-4xl lg:mt-8 text-blue-900 lg:mb-10 font-bold">Further reading</h2>
<ul class="mx-5 xl:flex xl:justify-start xl:gap-10">
{% for post in recommended_posts %}
<li class="flex-1">
+1 -1
View File
@@ -17,7 +17,7 @@
<a rel="prefetch" href="/{{segment}}/{{post.slug}}" class="text-blue-950 visited:text-purple-700 no-underline">{{post.metadata.title}}</a>
</h3>
</header>
<section class="text-base leading-7 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section>
<section class="text-base leading-5 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section>
<footer class="text-sm md:text-base lg:text-lg mt-3 sm:mt-0 clear-both sm:clear-none">
<ul class="inline-block" style="view-transition-name: post_tags_{{post.slug}}">
{% for tag in post.metadata.tags %}
+18
View File
@@ -0,0 +1,18 @@
<section class="flex border rounded bg-white p-3">
<aside class="flex justify-center items-center pr-3 shrink-0">
{% match education.thumbnail %}
{% when Some with (source) %}
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "education 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">
{{picture|safe}}
</figure>
{% when None %}
{% endmatch %}
</aside>
<section>
<header>
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{education.name}}</h3>
</header>
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{education.description|parse_markdown|safe}}</section>
</section>
</section>
@@ -1,4 +1,4 @@
<section class="border border-slate-200 rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
<section class="border rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
<header class="px-4 mb-3">
<h2 class="text-xl font-semibold text-blue-900 md:text-2xl">
{% match project.metadata.link %}
+3 -2
View File
@@ -1,8 +1,9 @@
<section class="flex border border-slate-200 rounded bg-white p-3">
<section class="flex border rounded bg-white p-3">
<aside class="flex justify-center items-center pr-3 shrink-0">
{% match skill.thumbnail %}
{% when Some with (source) %}
{% 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 skill_name = skill.name.clone() %}
{% 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">
{{picture|safe}}
</figure>
+2 -2
View File
@@ -1,6 +1,6 @@
{% macro social_card_start(svg, url, heading, img, class) %}
<a href="{{url}}" class="block no-underline border border-slate-200 rounded-md bg-pink-200 m-4 p-4 max-w-[392px] {{class}}">
<a href="{{url}}" class="block no-underline border rounded-md bg-pink-200 m-4 p-4 max-w-[392px] {{class}}">
<header class="flex text-center justify-center items-center gap-2 mb-2">
<svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="h-7 w-7 fill-blue-950">
<use href="#{{svg}}" />
@@ -9,6 +9,6 @@
</header>
{% let alt_text = format!("{svg} thumbnail") %}
{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-xs")).unwrap_or("thumbnail not found".to_string())|safe }}
{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-sm")).unwrap_or("thumbnail not found".to_string())|safe }}
</a>
{% endmacro %}
+1 -1
View File
@@ -1,5 +1,5 @@
{% macro talent_card(svg, heading, description) %}
<section class="flex border border-slate-200 rounded-sm bg-white p-3">
<section class="flex border rounded bg-white p-3">
<aside class="flex justify-center items-center pr-3">
<svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="fill-blue-950 h-12 w-12 md:h-16 md:w-16">
<use href="#{{svg}}" />
+18
View File
@@ -0,0 +1,18 @@
<section class="flex border rounded bg-white p-3">
<aside class="flex justify-center items-center pr-3 shrink-0">
{% match workplace.thumbnail %}
{% when Some with (source) %}
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "Workplace 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">
{{picture|safe}}
</figure>
{% when None %}
{% endmatch %}
</aside>
<section>
<header>
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{workplace.name}}</h3>
</header>
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{workplace.description|parse_markdown|safe}}</section>
</section>
</section>
+1 -1
View File
@@ -174,7 +174,7 @@
<ul class="m-6 flex gap-2 flex-wrap justify-center">
{% for technology in technology_list %}
<li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded-sm border-blue-300 font-mono">
<li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded border-blue-300 font-mono">
{{technology}}
</li>
{% endfor %}
+1 -1
View File
@@ -10,7 +10,7 @@
</a>
{% when None %}
{% endmatch %}
<aside class="flex logo-section grow justify-end content-end">
<aside class="flex logo-section flex-grow justify-end content-end">
<a class="logo p-3 text-base" href="/">
@michalvankodev
</a>