Compare commits
6 Commits
8c4c4af56a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 843991f0ba | |||
| 3ee285dec0 | |||
| 1fff7e0704 | |||
| 32dbece9e1 | |||
| 2ff9139f73 | |||
| 4192a19fe8 |
@@ -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()],
|
||||||
},
|
},
|
||||||
|
|||||||
28
package.json
@@ -5,26 +5,26 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"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"
|
"tailwind-animations": "^1.0.1",
|
||||||
|
"tailwindcss": "^4.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1430
pnpm-lock.yaml
generated
6
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
packages:
|
||||||
|
- .
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- sharp
|
||||||
|
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 599 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -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,50 +12,42 @@ 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"
|
||||||
>
|
>
|
||||||
Led frontend development for Ask Chief, shaping AI product experiences with scalable interfaces
|
Frontend work on Ask Chief, building interfaces for AI-powered products that needed to feel simple
|
||||||
that could evolve alongside intelligent workflows.
|
while handling complex workflows underneath.
|
||||||
</PreviousRole>
|
</PreviousRole>
|
||||||
|
|
||||||
<PreviousRole
|
<PreviousRole
|
||||||
company="Celonis"
|
company="Celonis"
|
||||||
logo="/celonis-logo.webp"
|
logo={celonisLogo}
|
||||||
href="https://celonis.com"
|
href="https://celonis.com"
|
||||||
>
|
>
|
||||||
Built analytical product interfaces for OCPM, alongside shared component systems, build pipelines,
|
Building out analytical interfaces for OCPM, a shared component library, and tooling that made it
|
||||||
and platform foundations that helped teams move faster together.
|
easier for the whole team to ship.
|
||||||
</PreviousRole>
|
</PreviousRole>
|
||||||
|
|
||||||
<PreviousRole
|
<PreviousRole
|
||||||
company="Lenses.io"
|
company="Lenses.io"
|
||||||
logo="/lenses-logo.webp"
|
logo={lensesLogo}
|
||||||
href="https://lenses.io"
|
href="https://lenses.io"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-inside space-y-1">
|
Product interfaces for SQL Processors and Data Catalog, a shared component library, and developer
|
||||||
<li>Engineered product interfaces for SQL Processors and Data Catalog.</li>
|
tooling including the CLI.
|
||||||
<li>Architected the shared components library.</li>
|
|
||||||
<li>Worked on developer tooling, including the CLI.</li>
|
|
||||||
</ul>
|
|
||||||
</PreviousRole>
|
</PreviousRole>
|
||||||
|
|
||||||
<PreviousRole company="The Chat Shop" logo="/thechatshop-logo.jpg">
|
<PreviousRole company="The Chat Shop" logo={theChatShopLogo}>
|
||||||
<ul class="list-disc list-inside space-y-1">
|
Streaming data infrastructure built from scratch, along with a processing pipeline that kept delivery
|
||||||
<li>Spearheaded end-to-end processing architecture.</li>
|
running reliably at scale.
|
||||||
<li>Established the company's streaming data infrastructure.</li>
|
|
||||||
<li>Built the operational foundations for reliable delivery and automation.</li>
|
|
||||||
</ul>
|
|
||||||
</PreviousRole>
|
</PreviousRole>
|
||||||
|
|
||||||
<PreviousRole
|
<PreviousRole
|
||||||
company="Commversion"
|
company="Commversion"
|
||||||
logo="/commversion-logo.webp"
|
logo={commversionLogo}
|
||||||
href="https://commversion.com"
|
href="https://commversion.com"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-inside space-y-1">
|
Customer delivery platform and its public-facing side, plus InstantConnect, taken from design through
|
||||||
<li>Developed the customer delivery platform and its public-facing infrastructure.</li>
|
to implementation.
|
||||||
<li>Designed and implemented the initial InstantConnect system.</li>
|
|
||||||
</ul>
|
|
||||||
</PreviousRole>
|
</PreviousRole>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,29 +1,42 @@
|
|||||||
---
|
---
|
||||||
|
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
|
||||||
<img
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="block shrink-0">
|
||||||
|
<Image
|
||||||
src={logo}
|
src={logo}
|
||||||
alt={company}
|
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"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<Image
|
||||||
src={logo}
|
src={logo}
|
||||||
alt={company}
|
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"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -35,8 +48,7 @@ const { logo, href, company } = Astro.props;
|
|||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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}
|
{company}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
---
|
---
|
||||||
|
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}
|
||||||
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"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -23,8 +30,7 @@ const { title, image, url } = Astro.props;
|
|||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400 md:text-xs">
|
<div class="text-sm text-slate-500 dark:text-slate-400 md:text-xs">
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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()]),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ import { personJsonLd } from "@/utils/seo";
|
|||||||
title="Marios Antonoudiou — AI Product Engineer"
|
title="Marios Antonoudiou — AI Product Engineer"
|
||||||
description="AI product engineer building AI-powered products that feel simple, useful, and ready for real users."
|
description="AI product engineer building AI-powered products that feel simple, useful, and ready for real users."
|
||||||
withNav={false}
|
withNav={false}
|
||||||
jsonLd={personJsonLd()}
|
jsonLd={personJsonLd()}>
|
||||||
>
|
|
||||||
<HomeIntroduction />
|
<HomeIntroduction />
|
||||||
|
<div class="animate-fade-in animate-delay-150">
|
||||||
<HomeLatestArticle />
|
<HomeLatestArticle />
|
||||||
<HomeBeenWorkingWith />
|
</div>
|
||||||
<HomePersonalProjects />
|
<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 />
|
<HomeConnect />
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
|
@import "tailwind-animations";
|
||||||
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@
|
|||||||
@theme {
|
@theme {
|
||||||
--font-sans:
|
--font-sans:
|
||||||
Inter Variable, Inter, ui-sans-serif, system-ui, sans-serif,
|
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 {
|
@layer base {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||