Compare commits

...

6 Commits

Author SHA1 Message Date
843991f0ba Fix pnpm 2026-06-16 23:32:08 +03:00
3ee285dec0 Update image backgrounds 2026-06-16 23:22:48 +03:00
1fff7e0704 Add subtle animation on the homepage 2026-06-16 23:19:14 +03:00
32dbece9e1 Remove start script 2026-06-15 18:25:05 +03:00
2ff9139f73 upgrade deps to latest & migrate to astro:assets Image
- bump astro 5→6, @astrojs/mdx 4→6, astro-seo 0.8→1.1, sharp 0.33→0.35, plus minor updates
- move local images from public/ to src/assets/
- replace <img> with <Image> from astro:assets (inferSize for remote URLs)
- content schema uses image() helper for local covers
- eager-load above-the-fold images (article covers, hero avatars)
2026-06-15 18:00:21 +03:00
4192a19fe8 Rewrite been-building-with section: replace bullets with prose, use natural tone 2026-06-15 15:38:56 +03:00
25 changed files with 637 additions and 1042 deletions

View File

@@ -7,6 +7,14 @@ import icon from "astro-icon";
export default defineConfig({
site: "https://mariosant.dev",
integrations: [mdx(), sitemap(), icon()],
image: {
domains: [
"images.unsplash.com",
"app.ghostwriter.rocks",
"990141.apps.zdusercontent.com",
"cdn.livechat-files.com",
],
},
vite: {
plugins: [tailwindcss()],
},

View File

@@ -5,26 +5,26 @@
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.2.0",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"@fontsource-variable/inter": "^5.2.5",
"@iconify-json/heroicons": "^1.2.1",
"@astrojs/mdx": "^6.0.3",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.3",
"@fontsource-variable/inter": "^5.2.8",
"@iconify-json/heroicons": "^1.2.3",
"@iconify-json/hugeicons": "^1.2.29",
"@iconify-json/mynaui": "^1.2.5",
"@iconify-json/simple-icons": "^1.2.28",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.14",
"astro": "^5.5.2",
"@iconify-json/mynaui": "^1.2.17",
"@iconify-json/simple-icons": "^1.2.86",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.1",
"astro": "^6.4.7",
"astro-icon": "^1.1.5",
"astro-seo": "^0.8.4",
"sharp": "^0.33.5",
"tailwindcss": "^4.0.14"
"astro-seo": "^1.1.0",
"sharp": "^0.35.1",
"tailwind-animations": "^1.0.1",
"tailwindcss": "^4.3.1"
}
}

1430
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,6 @@
packages:
- .
onlyBuiltDependencies:
- esbuild
- sharp

View File

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 599 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,5 +1,10 @@
---
import PreviousRole from "@/components/previous-role.astro";
import lateralusLogo from "@/assets/lateralus-logo.jpeg";
import celonisLogo from "@/assets/celonis-logo.webp";
import lensesLogo from "@/assets/lenses-logo.webp";
import theChatShopLogo from "@/assets/thechatshop-logo.jpg";
import commversionLogo from "@/assets/commversion-logo.webp";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
@@ -7,50 +12,42 @@ import PreviousRole from "@/components/previous-role.astro";
<PreviousRole
company="Lateralus Ventures"
logo="/lateralus-logo.jpeg"
logo={lateralusLogo}
href="https://lateralus.ventures"
>
Led frontend development for Ask Chief, shaping AI product experiences with scalable interfaces
that could evolve alongside intelligent workflows.
Frontend work on Ask Chief, building interfaces for AI-powered products that needed to feel simple
while handling complex workflows underneath.
</PreviousRole>
<PreviousRole
company="Celonis"
logo="/celonis-logo.webp"
logo={celonisLogo}
href="https://celonis.com"
>
Built analytical product interfaces for OCPM, alongside shared component systems, build pipelines,
and platform foundations that helped teams move faster together.
Building out analytical interfaces for OCPM, a shared component library, and tooling that made it
easier for the whole team to ship.
</PreviousRole>
<PreviousRole
company="Lenses.io"
logo="/lenses-logo.webp"
logo={lensesLogo}
href="https://lenses.io"
>
<ul class="list-disc list-inside space-y-1">
<li>Engineered product interfaces for SQL Processors and Data Catalog.</li>
<li>Architected the shared components library.</li>
<li>Worked on developer tooling, including the CLI.</li>
</ul>
Product interfaces for SQL Processors and Data Catalog, a shared component library, and developer
tooling including the CLI.
</PreviousRole>
<PreviousRole company="The Chat Shop" logo="/thechatshop-logo.jpg">
<ul class="list-disc list-inside space-y-1">
<li>Spearheaded end-to-end processing architecture.</li>
<li>Established the company's streaming data infrastructure.</li>
<li>Built the operational foundations for reliable delivery and automation.</li>
</ul>
<PreviousRole company="The Chat Shop" logo={theChatShopLogo}>
Streaming data infrastructure built from scratch, along with a processing pipeline that kept delivery
running reliably at scale.
</PreviousRole>
<PreviousRole
company="Commversion"
logo="/commversion-logo.webp"
logo={commversionLogo}
href="https://commversion.com"
>
<ul class="list-disc list-inside space-y-1">
<li>Developed the customer delivery platform and its public-facing infrastructure.</li>
<li>Designed and implemented the initial InstantConnect system.</li>
</ul>
Customer delivery platform and its public-facing side, plus InstantConnect, taken from design through
to implementation.
</PreviousRole>
</section>

View File

@@ -1,16 +1,18 @@
---
import { Image } from "astro:assets";
import Button from "@/components/button.astro";
import Link from "@/components/link.astro";
import mariosant from "@/assets/mariosant.webp";
---
<header class="flex flex-col md:gap-12 gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="flex items-center justify-between">
<a href="/" aria-label="Home">
<img
<Image
src={mariosant}
alt="Marios Antonoudiou"
src="/mariosant.webp"
width="64"
height="64"
width={64}
height={64}
class="w-16 h-16 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager"
/>

View File

@@ -1,4 +1,5 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import { formatDoMMMMYYYY } from "@/utils/date";
@@ -12,7 +13,10 @@ if (!article) {
}
const cover = article.data.coverImage;
const coverUrl = cover.url.replace("w=1287&h=600", "w=300&h=300");
const isRemote = typeof cover.url === "string";
const coverSrc = isRemote
? cover.url.replace("w=1287&h=600", "w=300&h=300")
: cover.url;
const body = (article.body ?? "").trim();
const excerpt = body.length > 150 ? `${body.slice(0, 150)}…` : body;
@@ -30,10 +34,11 @@ const excerpt = body.length > 150 ? `${body.slice(0, 150)}…` : body;
<div class="flex gap-6 items-start flex-row-reverse">
{
cover.url && (
<img
<Image
class="w-24 h-24 object-cover rounded-lg shrink-0"
src={coverUrl}
src={coverSrc}
alt={article.data.title}
inferSize={isRemote}
loading="lazy"
/>
)

View File

@@ -1,29 +1,42 @@
---
import { Image, type ImageMetadata } from "astro:assets";
export interface Props {
logo: string;
logo: ImageMetadata | string;
href?: string;
company: string;
}
const { logo, href, company } = Astro.props;
const isRemote = typeof logo === "string";
---
<div class="flex gap-5">
{
href ? (
<a href={href} target="_blank" rel="noopener noreferrer" class="block shrink-0">
<img
<a
href={href}
target="_blank"
rel="noopener noreferrer"
class="block shrink-0">
<Image
src={logo}
alt={company}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white"
inferSize={isRemote}
width={isRemote ? undefined : 50}
height={isRemote ? undefined : 50}
class="block rounded size-12 object-contain"
loading="lazy"
/>
</a>
) : (
<img
<Image
src={logo}
alt={company}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white shrink-0"
inferSize={isRemote}
width={isRemote ? undefined : 50}
height={isRemote ? undefined : 50}
class="block rounded size-12 object-contain shrink-0"
loading="lazy"
/>
)
@@ -35,8 +48,7 @@ const { logo, href, company } = Astro.props;
href={href}
target="_blank"
rel="noopener noreferrer"
class="md:text-xl text-lg font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white"
>
class="md:text-xl text-lg font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white">
{company}
</a>
) : (

View File

@@ -1,19 +1,26 @@
---
import { Image, type ImageMetadata } from "astro:assets";
export interface Props {
title: string;
image: string;
image: ImageMetadata | string;
url: string;
}
const { title, image, url } = Astro.props;
const isRemote = typeof image === "string";
---
<div class="flex overflow-auto gap-x-5 md:gap-2 items-center md:items-start md:flex-col md:border md:rounded-lg md:border-gray-200 md:p-4 md:dark:border-slate-600">
<div
class="flex overflow-auto gap-x-5 md:gap-2 items-center md:items-start md:flex-col md:border md:rounded-lg md:border-gray-200 md:p-4 md:dark:border-slate-600">
<a href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
<img
<Image
src={image}
alt={title}
class="block min-w-[50px] w-[50px] md:w-16 h-[50px] md:h-16 object-contain bg-white rounded"
inferSize={isRemote}
width={isRemote ? undefined : 64}
height={isRemote ? undefined : 64}
class="block min-w-[50px] w-[50px] md:w-16 h-[50px] md:h-16 object-contain rounded"
loading="lazy"
/>
</a>
@@ -23,8 +30,7 @@ const { title, image, url } = Astro.props;
href={url}
target="_blank"
rel="noopener noreferrer"
class="font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white"
>
class="font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white">
{title}
</a>
<div class="text-sm text-slate-500 dark:text-slate-400 md:text-xs">

View File

@@ -1,5 +1,7 @@
---
import { Image } from "astro:assets";
import Link from "@/components/link.astro";
import mariosant from "@/assets/mariosant.webp";
const path = Astro.url.pathname;
const isHome = path === "/" || path === "";
@@ -9,11 +11,11 @@ const isArticles = path === "/articles" || path.startsWith("/articles/");
<div class="sticky top-0 z-10 border-b border-gray-200 dark:border-slate-600 bg-white/85 dark:bg-slate-800/90 backdrop-blur-sm">
<nav class="grid grid-cols-2 items-center gap-3 !py-2 max-w-3xl mx-auto w-full p-4 sm:p-6 lg:p-8">
<Link href="/" class="flex gap-3 items-center" variant="default">
<img
<Image
src={mariosant}
alt="Marios Antonoudiou"
src="/mariosant.webp"
width="40"
height="40"
width={40}
height={40}
class="w-10 h-10 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager"
/>

View File

@@ -3,14 +3,15 @@ import { glob } from "astro/loaders";
const articles = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/articles" }),
schema: z.object({
schema: ({ image }) =>
z.object({
title: z.string(),
date: z.coerce.date(),
description: z.string().optional(),
coverImage: z.object({
author: z.string(),
authorUrl: z.string().url().optional(),
url: z.string(),
url: z.union([image(), z.string().url()]),
}),
}),
});

View File

@@ -3,7 +3,7 @@ title: Smooth Scrolling in Agentic Chat Interfaces
date: 2025-09-15
coverImage:
author: mariosant
url: "/agentic-ui.png"
url: "../../assets/agentic-ui.png"
---
When building conversational UIs, small interaction details can make a huge difference in how natural the experience feels. Recently, while working on an agentic chat interface, I ran into a deceptively simple requirement:

View File

@@ -1,10 +1,11 @@
---
import { Image } from "astro:assets";
import { getCollection, render } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Link from "@/components/link.astro";
import Button from "@/components/button.astro";
import { formatDoMMMMYYYY } from "@/utils/date";
import { articleJsonLd, breadcrumbJsonLd, deriveDescription } from "@/utils/seo";
import { articleJsonLd, breadcrumbJsonLd, deriveDescription, imageSrc } from "@/utils/seo";
export async function getStaticPaths() {
const articles = await getCollection("articles");
@@ -17,7 +18,8 @@ export async function getStaticPaths() {
const { entry } = Astro.props;
const { Content } = await render(entry);
const cover = entry.data.coverImage;
const showCover = Boolean(cover?.url);
const isRemote = typeof cover.url === "string";
const showCover = isRemote ? Boolean(cover.url) : true;
const description = entry.data.description ?? deriveDescription(entry.body ?? "");
---
@@ -25,7 +27,7 @@ const description = entry.data.description ?? deriveDescription(entry.body ?? ""
title={entry.data.title}
description={description}
ogType="article"
ogImage={cover.url}
ogImage={imageSrc(cover.url)}
ogImageAlt={entry.data.title}
jsonLd={[articleJsonLd(entry), breadcrumbJsonLd(entry)]}
>
@@ -48,13 +50,12 @@ const description = entry.data.description ?? deriveDescription(entry.body ?? ""
{
showCover && (
<figure class="hidden md:block max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
<img
<Image
src={cover.url}
alt={entry.data.title}
height="1000"
width="1700"
inferSize={isRemote}
class="rounded-lg w-full h-auto"
loading="lazy"
loading="eager"
/>
{cover.authorUrl && (
<a

View File

@@ -1,4 +1,5 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import { formatDoMMMMYYYY } from "@/utils/date";
@@ -20,14 +21,18 @@ const articles = (await getCollection("articles")).sort(
<section class="grid grid-cols-12 gap-8 max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
{
articles.map((article) => (
articles.map((article, index) => {
const url = article.data.coverImage.url;
const isRemote = typeof url === "string";
return (
<article class="col-span-12 md:col-span-6 border border-gray-200 dark:border-slate-600 rounded-lg overflow-hidden !p-0">
<a href={`/articles/${article.id}`} class="block cursor-pointer">
<img
<Image
class="w-full h-48 object-cover"
src={article.data.coverImage.url}
src={url}
alt={article.data.title}
loading="lazy"
inferSize={isRemote}
loading={index < 2 ? "eager" : "lazy"}
/>
</a>
<div class="grid gap-4 p-4 md:p-6">
@@ -52,7 +57,8 @@ const articles = (await getCollection("articles")).sort(
</div>
</div>
</article>
))
);
})
}
</section>
</Layout>

View File

@@ -12,11 +12,18 @@ import { personJsonLd } from "@/utils/seo";
title="Marios Antonoudiou — AI Product Engineer"
description="AI product engineer building AI-powered products that feel simple, useful, and ready for real users."
withNav={false}
jsonLd={personJsonLd()}
>
jsonLd={personJsonLd()}>
<HomeIntroduction />
<div class="animate-fade-in animate-delay-150">
<HomeLatestArticle />
<HomeBeenWorkingWith />
<HomePersonalProjects />
</div>
<div class="animate-fade-in animate-delay-250">
<HomeBeenWorkingWith class="animate-fade-in animate-delay-150" />
</div>
<div class="animate-fade-in animate-delay-350">
<HomePersonalProjects class="animate-fade-in animate-delay-150" />
</div>
<div class="animate-fade-in animate-delay-450">
<HomeConnect />
</div>
</Layout>

View File

@@ -1,7 +1,6 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { SITE_URL } from "@/utils/seo";
import { deriveDescription } from "@/utils/seo";
import { SITE_URL, imageSrc, deriveDescription } from "@/utils/seo";
import type { APIContext } from "astro";
export async function GET(context: APIContext) {
@@ -22,7 +21,7 @@ export async function GET(context: APIContext) {
content: undefined,
author: "mariosant@sent.com (Marios Antonoudiou)",
categories: [],
customData: `<enclosure url="${new URL(entry.data.coverImage.url, context.site ?? SITE_URL).toString()}" type="image/jpeg" />`,
customData: `<enclosure url="${new URL(imageSrc(entry.data.coverImage.url), context.site ?? SITE_URL).toString()}" type="image/jpeg" />`,
})),
customData: `<language>en-us</language>`,
stylesheet: false,

View File

@@ -1,4 +1,5 @@
@import 'tailwindcss';
@import "tailwindcss";
@import "tailwind-animations";
@custom-variant dark (&:where(.dark, .dark *));
@@ -7,7 +8,7 @@
@theme {
--font-sans:
Inter Variable, Inter, ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@layer base {

View File

@@ -1,4 +1,5 @@
import type { CollectionEntry } from "astro:content";
import type { ImageMetadata } from "astro:assets";
export const SITE_URL = "https://mariosant.dev";
export const TWITTER_HANDLE = "@marios_ant";
@@ -42,6 +43,9 @@ export const deriveDescription = (body: string, max = 155): string => {
type ArticleEntry = CollectionEntry<"articles">;
export const imageSrc = (img: ImageMetadata | string): string =>
typeof img === "string" ? img : img.src;
export const personJsonLd = (): string =>
JSON.stringify({
"@context": "https://schema.org",
@@ -62,7 +66,7 @@ export const articleJsonLd = (entry: ArticleEntry): string => {
"@type": "Article",
headline: entry.data.title,
description,
image: entry.data.coverImage.url,
image: imageSrc(entry.data.coverImage.url),
datePublished: entry.data.date.toISOString(),
author: {
"@id": PERSON_ID,