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)
This commit is contained in:
2026-06-15 18:00:21 +03:00
parent 4192a19fe8
commit 2ff9139f73
22 changed files with 580 additions and 1006 deletions

View File

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

View File

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

1418
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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 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"> <section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
@@ -7,7 +12,7 @@ import PreviousRole from "@/components/previous-role.astro";
<PreviousRole <PreviousRole
company="Lateralus Ventures" company="Lateralus Ventures"
logo="/lateralus-logo.jpeg" logo={lateralusLogo}
href="https://lateralus.ventures" href="https://lateralus.ventures"
> >
Frontend work on Ask Chief, building interfaces for AI-powered products that needed to feel simple Frontend work on Ask Chief, building interfaces for AI-powered products that needed to feel simple
@@ -16,7 +21,7 @@ import PreviousRole from "@/components/previous-role.astro";
<PreviousRole <PreviousRole
company="Celonis" company="Celonis"
logo="/celonis-logo.webp" logo={celonisLogo}
href="https://celonis.com" href="https://celonis.com"
> >
Building out analytical interfaces for OCPM, a shared component library, and tooling that made it Building out analytical interfaces for OCPM, a shared component library, and tooling that made it
@@ -25,21 +30,21 @@ import PreviousRole from "@/components/previous-role.astro";
<PreviousRole <PreviousRole
company="Lenses.io" company="Lenses.io"
logo="/lenses-logo.webp" logo={lensesLogo}
href="https://lenses.io" href="https://lenses.io"
> >
Product interfaces for SQL Processors and Data Catalog, a shared component library, and developer Product interfaces for SQL Processors and Data Catalog, a shared component library, and developer
tooling including the CLI. tooling including the CLI.
</PreviousRole> </PreviousRole>
<PreviousRole company="The Chat Shop" logo="/thechatshop-logo.jpg"> <PreviousRole company="The Chat Shop" logo={theChatShopLogo}>
Streaming data infrastructure built from scratch, along with a processing pipeline that kept delivery Streaming data infrastructure built from scratch, along with a processing pipeline that kept delivery
running reliably at scale. running reliably at scale.
</PreviousRole> </PreviousRole>
<PreviousRole <PreviousRole
company="Commversion" company="Commversion"
logo="/commversion-logo.webp" logo={commversionLogo}
href="https://commversion.com" href="https://commversion.com"
> >
Customer delivery platform and its public-facing side, plus InstantConnect, taken from design through Customer delivery platform and its public-facing side, plus InstantConnect, taken from design through

View File

@@ -1,16 +1,18 @@
--- ---
import { Image } from "astro:assets";
import Button from "@/components/button.astro"; import Button from "@/components/button.astro";
import Link from "@/components/link.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"> <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"> <div class="flex items-center justify-between">
<a href="/" aria-label="Home"> <a href="/" aria-label="Home">
<img <Image
src={mariosant}
alt="Marios Antonoudiou" alt="Marios Antonoudiou"
src="/mariosant.webp" width={64}
width="64" height={64}
height="64"
class="w-16 h-16 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block" class="w-16 h-16 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager" loading="eager"
/> />

View File

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

View File

@@ -1,28 +1,37 @@
--- ---
import { Image, type ImageMetadata } from "astro:assets";
export interface Props { export interface Props {
logo: string; logo: ImageMetadata | string;
href?: string; href?: string;
company: string; company: string;
} }
const { logo, href, company } = Astro.props; const { logo, href, company } = Astro.props;
const isRemote = typeof logo === "string";
--- ---
<div class="flex gap-5"> <div class="flex gap-5">
{ {
href ? ( href ? (
<a href={href} target="_blank" rel="noopener noreferrer" class="block shrink-0"> <a href={href} target="_blank" rel="noopener noreferrer" class="block shrink-0">
<img <Image
src={logo} src={logo}
alt={company} alt={company}
inferSize={isRemote}
width={isRemote ? undefined : 50}
height={isRemote ? undefined : 50}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white" class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white"
loading="lazy" loading="lazy"
/> />
</a> </a>
) : ( ) : (
<img <Image
src={logo} src={logo}
alt={company} alt={company}
inferSize={isRemote}
width={isRemote ? undefined : 50}
height={isRemote ? undefined : 50}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white shrink-0" class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white shrink-0"
loading="lazy" loading="lazy"
/> />

View File

@@ -1,18 +1,24 @@
--- ---
import { Image, type ImageMetadata } from "astro:assets";
export interface Props { export interface Props {
title: string; title: string;
image: string; image: ImageMetadata | string;
url: string; url: string;
} }
const { title, image, url } = Astro.props; 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"> <a href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
<img <Image
src={image} src={image}
alt={title} alt={title}
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 bg-white rounded" class="block min-w-[50px] w-[50px] md:w-16 h-[50px] md:h-16 object-contain bg-white rounded"
loading="lazy" loading="lazy"
/> />

View File

@@ -1,5 +1,7 @@
--- ---
import { Image } from "astro:assets";
import Link from "@/components/link.astro"; import Link from "@/components/link.astro";
import mariosant from "@/assets/mariosant.webp";
const path = Astro.url.pathname; const path = Astro.url.pathname;
const isHome = path === "/" || path === ""; 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"> <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"> <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"> <Link href="/" class="flex gap-3 items-center" variant="default">
<img <Image
src={mariosant}
alt="Marios Antonoudiou" alt="Marios Antonoudiou"
src="/mariosant.webp" width={40}
width="40" height={40}
height="40"
class="w-10 h-10 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block" class="w-10 h-10 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager" loading="eager"
/> />

View File

@@ -3,14 +3,15 @@ import { glob } from "astro/loaders";
const articles = defineCollection({ const articles = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/articles" }), loader: glob({ pattern: "**/*.md", base: "./src/content/articles" }),
schema: z.object({ schema: ({ image }) =>
z.object({
title: z.string(), title: z.string(),
date: z.coerce.date(), date: z.coerce.date(),
description: z.string().optional(), description: z.string().optional(),
coverImage: z.object({ coverImage: z.object({
author: z.string(), author: z.string(),
authorUrl: z.string().url().optional(), 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 date: 2025-09-15
coverImage: coverImage:
author: mariosant 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: 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 { getCollection, render } from "astro:content";
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import Link from "@/components/link.astro"; import Link from "@/components/link.astro";
import Button from "@/components/button.astro"; import Button from "@/components/button.astro";
import { formatDoMMMMYYYY } from "@/utils/date"; import { formatDoMMMMYYYY } from "@/utils/date";
import { articleJsonLd, breadcrumbJsonLd, deriveDescription } from "@/utils/seo"; import { articleJsonLd, breadcrumbJsonLd, deriveDescription, imageSrc } from "@/utils/seo";
export async function getStaticPaths() { export async function getStaticPaths() {
const articles = await getCollection("articles"); const articles = await getCollection("articles");
@@ -17,7 +18,8 @@ export async function getStaticPaths() {
const { entry } = Astro.props; const { entry } = Astro.props;
const { Content } = await render(entry); const { Content } = await render(entry);
const cover = entry.data.coverImage; 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 ?? ""); const description = entry.data.description ?? deriveDescription(entry.body ?? "");
--- ---
@@ -25,7 +27,7 @@ const description = entry.data.description ?? deriveDescription(entry.body ?? ""
title={entry.data.title} title={entry.data.title}
description={description} description={description}
ogType="article" ogType="article"
ogImage={cover.url} ogImage={imageSrc(cover.url)}
ogImageAlt={entry.data.title} ogImageAlt={entry.data.title}
jsonLd={[articleJsonLd(entry), breadcrumbJsonLd(entry)]} jsonLd={[articleJsonLd(entry), breadcrumbJsonLd(entry)]}
> >
@@ -48,13 +50,12 @@ const description = entry.data.description ?? deriveDescription(entry.body ?? ""
{ {
showCover && ( showCover && (
<figure class="hidden md:block max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full"> <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} src={cover.url}
alt={entry.data.title} alt={entry.data.title}
height="1000" inferSize={isRemote}
width="1700"
class="rounded-lg w-full h-auto" class="rounded-lg w-full h-auto"
loading="lazy" loading="eager"
/> />
{cover.authorUrl && ( {cover.authorUrl && (
<a <a

View File

@@ -1,4 +1,5 @@
--- ---
import { Image } from "astro:assets";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import { formatDoMMMMYYYY } from "@/utils/date"; 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"> <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"> <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"> <a href={`/articles/${article.id}`} class="block cursor-pointer">
<img <Image
class="w-full h-48 object-cover" class="w-full h-48 object-cover"
src={article.data.coverImage.url} src={url}
alt={article.data.title} alt={article.data.title}
loading="lazy" inferSize={isRemote}
loading={index < 2 ? "eager" : "lazy"}
/> />
</a> </a>
<div class="grid gap-4 p-4 md:p-6"> <div class="grid gap-4 p-4 md:p-6">
@@ -52,7 +57,8 @@ const articles = (await getCollection("articles")).sort(
</div> </div>
</div> </div>
</article> </article>
)) );
})
} }
</section> </section>
</Layout> </Layout>

View File

@@ -1,7 +1,6 @@
import rss from "@astrojs/rss"; import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_URL } from "@/utils/seo"; import { SITE_URL, imageSrc, deriveDescription } from "@/utils/seo";
import { deriveDescription } from "@/utils/seo";
import type { APIContext } from "astro"; import type { APIContext } from "astro";
export async function GET(context: APIContext) { export async function GET(context: APIContext) {
@@ -22,7 +21,7 @@ export async function GET(context: APIContext) {
content: undefined, content: undefined,
author: "mariosant@sent.com (Marios Antonoudiou)", author: "mariosant@sent.com (Marios Antonoudiou)",
categories: [], 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>`, customData: `<language>en-us</language>`,
stylesheet: false, stylesheet: false,

View File

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