claude again
This commit is contained in:
parent
df2267a968
commit
f3fc893675
81 changed files with 945 additions and 2813 deletions
103
src/lib/site.ts
103
src/lib/site.ts
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue