- 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)
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
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";
|
|
export const AUTHOR_NAME = "Marios Antonoudiou";
|
|
export const AUTHOR_JOB_TITLE = "AI Product Engineer";
|
|
|
|
export const SOCIALS = {
|
|
github: "https://github.com/mariosant",
|
|
linkedin: "https://www.linkedin.com/in/mariosant/",
|
|
bluesky: "https://bsky.app/profile/mariosant.bsky.social",
|
|
email: "mailto:mariosant@sent.com",
|
|
calendar: "https://cal.com/mariosant/30min",
|
|
} as const;
|
|
|
|
export const PERSON_ID = `${SITE_URL}/#person`;
|
|
export const AUTHOR_DESCRIPTION =
|
|
"AI product engineer building AI-powered products that feel simple, useful, and ready for real users.";
|
|
|
|
export const stripMarkdown = (body: string): string =>
|
|
body
|
|
.replace(/```[\s\S]*?```/g, " ")
|
|
.replace(/`[^`]*`/g, " ")
|
|
.replace(/^#{1,6}\s+.*$/gm, "")
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
|
|
.replace(/^>\s*/gm, "")
|
|
.replace(/^\s*[-*+]\s+/gm, "")
|
|
.replace(/^\s*\d+\.\s+/gm, "")
|
|
.replace(/[*_~]+/g, "")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
|
|
export const deriveDescription = (body: string, max = 155): string => {
|
|
const plain = stripMarkdown(body);
|
|
if (plain.length <= max) return plain;
|
|
const cut = plain.slice(0, max);
|
|
const lastSpace = cut.lastIndexOf(" ");
|
|
const safeCut = lastSpace > 80 ? cut.slice(0, lastSpace) : cut;
|
|
return `${safeCut.trimEnd()}…`;
|
|
};
|
|
|
|
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",
|
|
"@type": "Person",
|
|
"@id": PERSON_ID,
|
|
name: AUTHOR_NAME,
|
|
jobTitle: AUTHOR_JOB_TITLE,
|
|
url: SITE_URL,
|
|
image: `${SITE_URL}/mariosant.webp`,
|
|
description: AUTHOR_DESCRIPTION,
|
|
sameAs: [SOCIALS.github, SOCIALS.linkedin, SOCIALS.bluesky],
|
|
});
|
|
|
|
export const articleJsonLd = (entry: ArticleEntry): string => {
|
|
const description = entry.data.description ?? deriveDescription(entry.body ?? "");
|
|
return JSON.stringify({
|
|
"@context": "https://schema.org",
|
|
"@type": "Article",
|
|
headline: entry.data.title,
|
|
description,
|
|
image: imageSrc(entry.data.coverImage.url),
|
|
datePublished: entry.data.date.toISOString(),
|
|
author: {
|
|
"@id": PERSON_ID,
|
|
"@type": "Person",
|
|
name: AUTHOR_NAME,
|
|
},
|
|
publisher: {
|
|
"@id": PERSON_ID,
|
|
"@type": "Person",
|
|
name: AUTHOR_NAME,
|
|
},
|
|
mainEntityOfPage: {
|
|
"@type": "WebPage",
|
|
"@id": `${SITE_URL}/articles/${entry.id}`,
|
|
},
|
|
});
|
|
};
|
|
|
|
export const breadcrumbJsonLd = (entry: ArticleEntry): string =>
|
|
JSON.stringify({
|
|
"@context": "https://schema.org",
|
|
"@type": "BreadcrumbList",
|
|
itemListElement: [
|
|
{
|
|
"@type": "ListItem",
|
|
position: 1,
|
|
name: "Home",
|
|
item: SITE_URL,
|
|
},
|
|
{
|
|
"@type": "ListItem",
|
|
position: 2,
|
|
name: "Articles",
|
|
item: `${SITE_URL}/articles`,
|
|
},
|
|
{
|
|
"@type": "ListItem",
|
|
position: 3,
|
|
name: entry.data.title,
|
|
item: `${SITE_URL}/articles/${entry.id}`,
|
|
},
|
|
],
|
|
});
|
|
|
|
export const articleListJsonLd = (entries: readonly ArticleEntry[]): string =>
|
|
JSON.stringify({
|
|
"@context": "https://schema.org",
|
|
"@type": "ItemList",
|
|
name: "Articles by Marios Antonoudiou",
|
|
itemListElement: entries.map((entry, index) => ({
|
|
"@type": "ListItem",
|
|
position: index + 1,
|
|
url: `${SITE_URL}/articles/${entry.id}`,
|
|
name: entry.data.title,
|
|
})),
|
|
});
|
|
|
|
export const renderJsonLd = (data: unknown): string => {
|
|
if (Array.isArray(data)) {
|
|
return data.map((item) => JSON.stringify(item)).join("\n");
|
|
}
|
|
return JSON.stringify(data);
|
|
};
|