Compare commits

...

10 Commits

Author SHA1 Message Date
Bethlenfalvi, Lorinc (ext)
8bafec4821 Updated footer and about
All checks were successful
Upload via SSH / build (push) Successful in 1m4s
2026-01-06 10:24:09 +01:00
Bethlenfalvi, Lorinc (ext)
d36c6d5622 maybe fixing job maybe
All checks were successful
Upload via SSH / build (push) Successful in 53s
2026-01-05 23:55:27 +01:00
Bethlenfalvi, Lorinc (ext)
e4d6a9df56 Switched to full sync
All checks were successful
Upload via SSH / build (push) Successful in 54s
2026-01-05 23:48:37 +01:00
Bethlenfalvi, Lorinc (ext)
77f9310786 Moved deploy to shared job
Some checks failed
Upload via SSH / build (push) Failing after 49s
2026-01-05 23:34:14 +01:00
Bethlenfalvi, Lorinc (ext)
967a9a3ebd Removed use of recent web apis
Some checks failed
Upload via SSH / build (push) Successful in 40s
Upload via SSH / deploy (push) Failing after 2s
2026-01-05 23:31:02 +01:00
Bethlenfalvi, Lorinc (ext)
0c3ac11c61 Various improvements
Some checks failed
Upload via SSH / build (push) Failing after 41s
Upload via SSH / deploy (push) Has been skipped
2026-01-05 23:23:01 +01:00
Bethlenfalvi, Lorinc (ext)
fe519c46e7 Added sync stage
Some checks failed
Upload via SSH / build (push) Failing after 30s
Upload via SSH / deploy (push) Has been skipped
2026-01-05 23:03:05 +01:00
Bethlenfalvi, Lorinc (ext)
01db7a8bb1 Updated packages
Some checks failed
Upload via SSH / build (push) Failing after 26s
Upload via SSH / deploy (push) Has been skipped
2026-01-05 22:59:15 +01:00
Bethlenfalvi, Lorinc (ext)
15aa4575bc Maybe fixed action
Some checks failed
Upload via SSH / build (push) Failing after 30s
Upload via SSH / deploy (push) Has been skipped
2026-01-05 22:33:45 +01:00
Bethlenfalvi, Lorinc (ext)
6f687d22c2 Updated action
Some checks failed
Upload via SSH / build (push) Failing after 5m20s
Upload via SSH / deploy (push) Has been skipped
2026-01-05 22:18:34 +01:00
14 changed files with 1945 additions and 1978 deletions

View File

@@ -0,0 +1,42 @@
name: Upload via SSH
on:
# Trigger the workflow every time you push to the `main` branch
# Using a different branch name? Replace `main` with your branchs name
push:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab on GitHub.
workflow_dispatch:
# Allow this job to clone the repo and create a page deployment
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install node
run: |
apt-get update
apt-get install nodejs -y
- name: Checkout your repository using git
uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Sync collection types
run: npx astro sync
- name: Build your site
run: npm run build
- name: Deploy
uses: https://code.klank.school/riviera/actions-file-deployer@v4
with:
remote-protocol: "sftp"
remote-host: "lbfalvy.com"
remote-user: "www-upload"
ssh-private-key: ${{ secrets.SSH_KEYFILE }}
local-path: "./dist"
remote-path: "/lbfalvy.com"
sync: full

View File

@@ -1,39 +0,0 @@
name: Deploy to GitHub Pages
on:
# Trigger the workflow every time you push to the `main` branch
# Using a different branch name? Replace `main` with your branchs name
push:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab on GitHub.
workflow_dispatch:
# Allow this job to clone the repo and create a page deployment
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout your repository using git
uses: actions/checkout@v4
- name: Install, build, and upload your site
uses: withastro/action@v3
# with:
# path: . # The root location of your Astro project inside the repository. (optional)
# node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 20. (optional)
# package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

3641
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,20 +9,20 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.0.8",
"@astrojs/react": "^4.2.0",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/svelte": "^7.0.4",
"@js-temporal/polyfill": "^0.4.4",
"@tailwindcss/vite": "^4.0.1",
"astro": "^5.2.1",
"@astrojs/mdx": "^4.3.13",
"@astrojs/react": "^4.4.2",
"@astrojs/rss": "^4.0.14",
"@astrojs/sitemap": "^3.6.0",
"@astrojs/svelte": "^7.2.4",
"@js-temporal/polyfill": "^0.5.1",
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.6",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-toc": "^9.0.0",
"shiki": "^2.2.0",
"svelte": "^5.19.6",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.3"
"shiki": "^3.20.0",
"svelte": "^5.46.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
}

View File

@@ -10,34 +10,46 @@
}
let { posts }: Props = $props();
let allTags = $derived([...new Set(posts.flatMap(item => item.data.tags))]);
let allTags = $derived([...new Set(posts.flatMap((item) => item.data.tags))]);
let selectedTags = $state<SvelteMap<string, boolean>>();
onMount(() => {
const url = new URL(window.location.href);
const tagsQry = url.searchParams.get('tags');
const defaultTags = tagsQry ? new Set(decodeURIComponent(tagsQry).split('+')) : new Set();
selectedTags = new SvelteMap(allTags.map(tag => [tag, defaultTags.has(tag)]));
})
const tagsQry = url.searchParams.get("tags");
const defaultTags = tagsQry
? new Set(decodeURIComponent(tagsQry).split("+"))
: new Set();
selectedTags = new SvelteMap(
allTags.map((tag) => [tag, defaultTags.has(tag)]),
);
});
$effect(() => {
if (!selectedTags) return;
const newTagsQry = encodeURIComponent(
[...selectedTags.entries()].filter(([_, v]) => v).map(([k, _]) => k).join('+')
[...selectedTags.entries()]
.filter(([_, v]) => v)
.map(([k, _]) => k)
.join("+"),
);
const url = new URL(window.location.href);
if (newTagsQry == url.searchParams.get("tags")) return;
if (newTagsQry == "") url.searchParams.delete("tags");
else url.searchParams.set("tags", newTagsQry);
window.history.pushState(null, "", url.toString());
})
});
let isFiltered = $derived(selectedTags && [...selectedTags.values()].includes(true));
let filteredPosts = $derived(isFiltered
? posts.filter(p => !p.data.unlisted && p.data.tags.some(t => selectedTags!.get(t)))
: posts.filter(p => !p.data.unlisted));
let shownPosts = $derived(filteredPosts.toReversed())
let isFiltered = $derived(
selectedTags && [...selectedTags.values()].includes(true),
);
let listedPosts = $derived(posts.filter((p) => !p.data.unlisted));
let filteredPosts = $derived(
isFiltered
? listedPosts.filter((p) => p.data.tags.some((t) => selectedTags!.get(t)))
: listedPosts,
);
let shownPosts = $derived([...filteredPosts].reverse());
</script>
<div class="lg:flex flex-row-reverse justify-between items-start">
@@ -46,31 +58,37 @@
{#each allTags as tag}
<button
onclick={() => {
console.log("TRoggled tag")
selectedTags!.set(tag, !selectedTags!.get(tag))
console.log("TRoggled tag");
selectedTags!.set(tag, !selectedTags!.get(tag));
}}
class={[
"m-0.5 rounded-4xl emph-bg px-2 cursor-pointer border-2 border-solid",
selectedTags?.get(tag) && "border-gray-400"
selectedTags?.get(tag) && "border-gray-400",
]}
>
{tag}
</button>
{/each}
</div>
<main class="max-w-[650px]">
<main class="max-w-650px">
{#each shownPosts as post}
<article>
<a href={`/blog/${post.id}`}>
<div class="
<div
class="
lg:grid grid-cols-[auto_min-content_min-content]
m-1 p-2 hover:bg-shade
">
"
>
<h2 class="font-bold">{post.data.title}</h2>
<address class="inline-block post-meta col-start-2 md:ml-3 whitespace-nowrap">
<address
class="inline-block post-meta col-start-2 md:ml-3 whitespace-nowrap"
>
{post.data.author}
</address>
<div class="inline-block post-meta col-start-3 lg:ml-1 whitespace-nowrap">
<div
class="inline-block post-meta col-start-3 lg:ml-1 whitespace-nowrap"
>
<Time datetime={post.data.pubDate} />
</div>
<div class="col-span-3">{post.data.summary}</div>

View File

@@ -71,7 +71,7 @@ function break_lines(input: string, width: number): string {
.join("\n")
}
export default function Fortune(): React.ReactElement {
export default function FortuneComponent(): React.ReactElement {
const id = Math.floor(Math.random() * messages.length);
window.localStorage.setItem("martinRefShown", id === 6 ? "yes" : "no");
const text = break_lines(messages[id], 60);

1
src/icons/Gitea_Logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"/> </g> </g> </g> </svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 960 B

5
src/icons/matrix.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" viewBox="4 7 42 36" width="50px"
height="50px">
<path
d="M 5 5 A 1.0001 1.0001 0 0 0 4 6 L 4 44 A 1.0001 1.0001 0 0 0 5 45 L 8 45 A 1.0001 1.0001 0 1 0 8 43 L 6 43 L 6 7 L 8 7 A 1.0001 1.0001 0 1 0 8 5 L 5 5 z M 42 5 A 1.0001 1.0001 0 1 0 42 7 L 44 7 L 44 43 L 42 43 A 1.0001 1.0001 0 1 0 42 45 L 45 45 A 1.0001 1.0001 0 0 0 46 44 L 46 6 A 1.0001 1.0001 0 0 0 45 5 L 42 5 z M 31.074219 17.509766 C 29.975744 17.487506 28.868391 17.760297 27.978516 18.373047 C 27.407516 18.767047 26.915609 19.272813 26.349609 19.757812 C 25.488609 18.039813 23.929344 17.580781 22.152344 17.550781 C 20.351344 17.519781 18.920922 18.341797 17.669922 19.841797 L 17.669922 18 L 14 18 L 14 32 L 17.664062 32 C 17.664062 32 17.657969 26.766016 17.667969 24.166016 C 17.669969 23.704016 17.689203 23.23225 17.783203 22.78125 C 18.073203 21.39225 19.538031 20.534437 20.957031 20.648438 C 22.309031 20.757437 23.100016 21.495656 23.166016 23.097656 C 23.177016 23.376656 23.166016 32 23.166016 32 L 26.832031 32 L 26.832031 24.228516 C 26.838031 23.629516 26.901875 23.0175 27.046875 22.4375 C 27.372875 21.1375 28.590531 20.49825 30.019531 20.65625 C 31.279531 20.79525 32.239031 21.474609 32.332031 22.849609 L 32.332031 32 L 36 32 L 36 22 C 36 21 35.746359 20.490859 35.443359 19.880859 C 34.710859 18.405234 32.90501 17.546865 31.074219 17.509766 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -13,7 +13,7 @@ const { title, author, summary, pubDate, updatedDate, image } = Astro.props;
<Main title={title} description={summary} image={image}>
<article class="m-5 mt-3">
<header
class="lg:grid grid-cols-[auto_auto_minmax(300px,_1fr)] grid-rows-[auto_auto]"
class="lg:grid grid-cols-[auto_auto_minmax(300px,1fr)] grid-rows-[auto_auto]"
>
<h2 class="font-bold row-span-2 text-2xl m-2 mt-3">{title}</h2>
<address class="post-meta inline-block col-start-2">{author}</address>
@@ -41,7 +41,7 @@ const { title, author, summary, pubDate, updatedDate, image } = Astro.props;
</p>
<a href="https://ko-fi.com/D1D4AFWZX" target="_blank">
<img
class="inline-block border-0 h-[24px]"
class="inline-block border-0 h-6"
src="https://storage.ko-fi.com/cdn/kofi1.png?v=6"
alt="Buy Me a Coffee at ko-fi.com"
/>
@@ -50,7 +50,7 @@ const { title, author, summary, pubDate, updatedDate, image } = Astro.props;
href="https://patreon.com/lbfalvy?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
>
<Image
class="inline-block border-0 h-[16px] w-[90px] object-cover ml-3"
class="inline-block border-0 h-4 w-22.5 object-cover ml-3"
src={patreonWordmark}
alt="Support me on Patreon"
/>

View File

@@ -3,9 +3,10 @@ import { Image } from "astro:assets";
import NavLink from "../components/NavLink.astro";
import Layout from "../layouts/Html.astro";
import "../styles/global.css";
import GhLogo from "../icons/github-mark-white.svg";
import GiteaLogo from "../icons/Gitea_Logo.svg";
import RssLogo from "../icons/rss.svg";
import BskyLogo from "../icons/Bluesky_Logo.svg";
import MatrixLogo from "../icons/matrix.svg";
import { SITE_DESCRIPTION, SITE_TITLE } from "../consts";
interface Props {
@@ -57,22 +58,22 @@ const {
<slot />
</main>
<footer class="md:row-start-2 emph-bg flex justify-center gap-3 py-3">
<a href="https://github.com/lbfalvy">
<a href="https://git.lbfalvy.com">
<Image
height="20"
width="20"
src={GhLogo}
alt="Github"
width="34"
src={GiteaLogo}
alt="Gitea"
loading="eager"
/>
</a>
<a href="/rss.xml">
<Image height="20" width="20" src={RssLogo} alt="Rss" loading="eager" />
</a>
<a href="https://bsky.app/profile/lbfalvy.bsky.social">
<a href="https://bsky.app/profile/lbfalvy.com">
<Image
height="20"
width="20"
width="25"
src={BskyLogo}
alt="BlueSky"
loading="eager"
@@ -87,6 +88,16 @@ const {
loading="eager"
/>
</a>
<a href="https://matrix.to/#/@lbfalvy:matrix.org">
<Image
height="20"
width="22"
color="white"
src={MatrixLogo}
alt="Chat over Matrix"
loading="eager"
/>
</a>
</footer>
</div>
</Layout>

View File

@@ -1,42 +1,34 @@
---
import { Image } from "astro:assets";
import Main from "../layouts/Main.astro";
---
<Main
title="About Me"
description="Lawrence Bethlenfalvy, software engineer"
>
<Main title="About Me" description="Lawrence Bethlenfalvy, software engineer">
<div class="max-w-[80ch] m-5">
<Image src='https://github.com/lbfalvy.png' loading="eager"
alt="My face" width="120" height="120"
class="rounded-full h-30 float-right m-3 [shape-outside:_ellipse()]" />
<Image
src="https://github.com/lbfalvy.png"
loading="eager"
alt="My face"
width="120"
height="120"
class="rounded-full h-30 float-right m-3 [shape-outside:ellipse()]"
/>
<p>
My name is Lawrence Bethlenfalvy, I make websites and web-based applications primarily
with React. I enjoy seeing the fruit of my labour, which is why I do so much
frontend development even though I'm not exactly an artistic genius, as finest
demonstrated by this website.
My name is Lawrence Bethlenfalvy, I make websites and web-based
applications primarily with React. I enjoy seeing the fruits of my labour,
which is why I do so much frontend development even though I'm not exactly
an artistic genius, as finest demonstrated by this website.
</p>
<p>
I really like perfectly designed infrastructure, and in an effort to
construct it for my own projects I've racked up a considerable amount of
DevOps experience.
I like well-oiled infrastructure, and in an effort to construct it for my
own projects I've racked up a considerable amount of DevOps experience.
</p>
<p>
I studied a lot of advanced mathematical topics in high school, and although
I was never a big fan of solving equations for hours on end the approach and
some of the concepts stuck with me. I like to draw on mathematical techniques
for day-to-day problem solving, but I also view it as a hobby.
</p>
<p>
More recently I've been hard at work on my small conceptual language, Orchid,
which, like anything I do, tries to be unopinionated, minimalistic in design,
and robust in execution.
Designing a programming language is a constant goat game against the halting problem
so realising all these principles at the same time is nigh impossible, and incremental
progress is fundamentally incompatible with the optimal, holistic approach to
issues like type checking, but when I have something to report I do it here.
More recently I've been hard at work on my small conceptual language,
Orchid, which, like anything I do, tries to be unopinionated, minimalistic
in design, and robust in execution. Incremental progress is fundamentally
incompatible with the optimal, holistic approach I like, but when I have
something to report I do it here.
</p>
</div>
</Main>

View File

@@ -1,8 +1,8 @@
---
import Main from "../layouts/Main.astro";
import Fortune from "../components/Fortune";
import FortuneComponent from "../components/Fortune";
---
<Main title="Fortune" description="Wisdom from Tux">
<Fortune client:only="react" />
<FortuneComponent client:only="react" />
</Main>

View File

@@ -15,16 +15,17 @@ const projReady = await Promise.all(
projReady.map(([proj, Content]) => (
<a href={proj.data.url}>
<article class="emph-bg m-3 p-2 text-sm rounded-2xl">
{proj.data.image && <Image
src={proj.data.image}
alt=""
loading="eager"
width="100" height="60"
class="object-center aspect-[16/9] w-full"
/>}
<h2 class="font-bold text-xl italic">
{proj.data.name}
</h2>
{proj.data.image && (
<Image
src={proj.data.image}
alt=""
loading="eager"
width="100"
height="60"
class="object-center aspect-video w-full"
/>
)}
<h2 class="font-bold text-xl italic">{proj.data.name}</h2>
<div>
<Content />
</div>