claude again

This commit is contained in:
Andras Schmelczer 2026-05-11 08:12:35 +01:00
parent df2267a968
commit f3fc893675
81 changed files with 945 additions and 2813 deletions

View file

@ -1,9 +1,11 @@
import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
import { getImage } from 'astro:assets';
import type { ImageMetadata } from 'astro';
export const site = {
name: 'Andras Schmelczer',
title: 'Andras Schmelczer — Software systems, AI, graphics, simulations, tools',
title: 'Andras Schmelczer — Software engineer',
description:
'Andras Schmelczer writes about software systems, AI deployment, graphics, simulations, and tools.',
url: 'https://schmelczer.dev',
@ -13,12 +15,22 @@ export const site = {
cv: '/media/downloads/cv-andras-schmelczer.pdf',
};
// Single source of truth for primary navigation. The Header consumes every
// entry where `footerOnly` is falsy AND `href !== '/'` (Home is implicit via
// the site title). The Footer renders every entry regardless. Items marked
// `footerOnly: true` appear only in the Footer.
export const navItems = [
{ href: '/', label: 'Home' },
{ href: '/articles/', label: 'Articles' },
{ href: '/projects/', label: 'Projects' },
{ href: '/about/', label: 'About' },
] as const;
{ href: '/tags/', label: 'Tags', footerOnly: false },
{ href: '/rss.xml', label: 'RSS', footerOnly: true },
] as const satisfies ReadonlyArray<{
href: string;
label: string;
footerOnly?: boolean;
}>;
export function formatDate(date: Date) {
return new Intl.DateTimeFormat('en', {
@ -59,8 +71,11 @@ export function tagPath(tag: string) {
return `/tags/${tagSlug(tag)}/`;
}
export function projectAnchor(project: CollectionEntry<'projects'>) {
return project.data.legacyAnchor ?? project.data.sourceProjectId;
// Anchor used for `id="..."` on project cards and `#fragment` deep links.
// Always derived from the canonical `sourceProjectId` slug now that the
// legacy anchor mapping has been dropped.
export function projectAnchor(slugOrEntry: string | CollectionEntry<'projects'>) {
return typeof slugOrEntry === 'string' ? slugOrEntry : slugOrEntry.data.sourceProjectId;
}
export function getAllTags(posts: { data: { tags: readonly string[] } }[]) {
@ -69,10 +84,20 @@ export function getAllTags(posts: { data: { tags: readonly string[] } }[]) {
);
}
export async function getPublishedPosts(): Promise<CollectionEntry<'posts'>[]> {
return (await getCollection('posts'))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
// Memoized published-posts loader. Build steps call `getPublishedPosts()`
// from many pages (index, articles, RSS, sitemap, tag pages, post layouts).
// Caching the promise means `getCollection('posts')` runs once per build.
let publishedPostsPromise: Promise<CollectionEntry<'posts'>[]> | undefined;
export function getPublishedPosts(): Promise<CollectionEntry<'posts'>[]> {
if (!publishedPostsPromise) {
publishedPostsPromise = getCollection('posts').then((posts) =>
posts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
);
}
return publishedPostsPromise;
}
export async function getProjects(): Promise<CollectionEntry<'projects'>[]> {
@ -114,3 +139,65 @@ export function getRelatedPosts(
export function absoluteUrl(path: string) {
return new URL(path, site.url).toString();
}
// Canonical Person JSON-LD. Used by the home page and About page; both share
// `@id` so search engines treat them as the same entity. Pass `extra` to
// add or override fields (e.g. `jobTitle`, richer `description`).
export function buildPersonJsonLd(extra?: Record<string, unknown>) {
return {
'@context': 'https://schema.org',
'@type': 'Person',
'@id': absoluteUrl('/about/#person'),
name: site.name,
url: site.url,
email: `mailto:${site.email}`,
sameAs: [site.github, site.linkedin],
description: site.description,
...extra,
};
}
// Wraps `getImage` with the standard OG dimensions (1200x630 JPEG). Used by
// Base.astro for the default OG image and by Post.astro for per-post
// thumbnails. Keeps OG output consistent across the site.
export function optimizeOgImage(src: ImageMetadata) {
return getImage({
src,
width: 1200,
height: 630,
format: 'jpg',
});
}
interface BreadcrumbCrumb {
name: string;
href: string;
}
interface BreadcrumbInput {
articles?: boolean;
tag?: string;
post?: CollectionEntry<'posts'>;
}
// Builds the breadcrumb trail shared by JSON-LD (BreadcrumbList) and the
// visible Breadcrumbs component. Home is always first. Pass `articles: true`
// to include the /articles/ crumb; pass a `tag` to append a tag crumb; pass
// a `post` to append the post title (linking to its article path).
export function buildBreadcrumbTrail({
articles,
tag,
post,
}: BreadcrumbInput): BreadcrumbCrumb[] {
const trail: BreadcrumbCrumb[] = [{ name: 'Home', href: '/' }];
if (articles || post) {
trail.push({ name: 'Articles', href: '/articles/' });
}
if (tag) {
trail.push({ name: tag, href: tagPath(tag) });
}
if (post) {
trail.push({ name: post.data.title, href: articlePath(post) });
}
return trail;
}