beeeeg update
Some checks failed
Test and deploy staging / staging (push) Has been cancelled

This commit is contained in:
2026-09-23 04:15:10 +00:00
parent 487eff5bb2
commit d0c7b6548b
135 changed files with 9531 additions and 1777 deletions

118
src/lib/seo.ts Normal file
View File

@@ -0,0 +1,118 @@
import { formatTitle, getTitle, type SiteEnvironment } from './title';
export const SITE_ORIGIN = 'https://stanrunge.dev';
type Article = {
title: string;
content: string;
updatedAt: Date | string;
};
type List = { name: string; isProgress: boolean; items: { name: string }[] };
type Race = { short: string; distance: number; discipline: string };
type PageContent = { blogArticle?: Article; list?: List; race?: Race };
type Seo = {
title: string;
description: string;
url: string;
indexable: boolean;
type: 'website' | 'article';
image: string;
imageAlt: string;
modifiedAt?: string;
};
const publicPages: Record<string, { title: string; description: string }> = {
'/': {
title: 'Projects, lists and writing',
description: 'Stan Runge’s projects, lists, writing and experience.'
},
'/projects': {
title: 'Projects',
description: 'Software projects by Stan Runge, including Catchemall, Animaldex and Derby Lens.'
},
'/blog': { title: 'Blog', description: 'Articles and notes by Stan Runge.' },
'/lists': { title: 'Lists', description: 'Lists collected and maintained by Stan Runge.' },
'/projects/catch-em-all': {
title: 'Catchemall',
description: 'Track which Pokémon you have caught across 53 games and 1,025 Pokémon.'
},
'/projects/animaldex': {
title: 'Animaldex',
description: 'Find and add neighborhood animals on a map.'
},
'/projects/derby': {
title: 'Derby Lens',
description: 'Estimate horse racing odds and compare them with market prices.'
},
'/projects/spotify-beatmap-finder': {
title: 'osu! map finder',
description: 'Find osu! beatmaps for Spotify songs, albums and playlists.'
}
};
function excerpt(markdown: string): string {
return markdown
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
.replace(/<[^>]*>/g, ' ')
.replace(/[`*_>#~|]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 160);
}
function date(value: Date | string): string | undefined {
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
}
export function getSeo(
pathname: string,
content: PageContent = {},
environment: SiteEnvironment = null
): Seo {
const path = pathname.replace(/\/$/, '') || '/';
const url = new URL(path, SITE_ORIGIN).href;
let page = publicPages[path];
let type: Seo['type'] = 'website';
let modifiedAt: string | undefined;
if (/^\/blog\/[^/]+$/.test(path) && content.blogArticle) {
const article = content.blogArticle;
page = {
title: article.title,
description: excerpt(article.content) || `Read ${article.title} by Stan Runge.`
};
type = 'article';
modifiedAt = date(article.updatedAt);
} else if (/^\/blog\/[^/]+\/edit$/.test(path) && content.blogArticle) {
page = { title: `Editing ${content.blogArticle.title}`, description: 'Edit a blog article.' };
} else if (/^\/lists\/[^/]+$/.test(path) && content.list) {
const list = content.list;
const names = list.items.slice(0, 3).map((item) => item.name);
page = {
title: list.name,
description: names.length
? `${list.name}: ${names.join(', ')}${list.items.length > names.length ? ', and more' : ''}.`
: `${list.name}, a ${list.isProgress ? 'progress ' : ''}list by Stan Runge.`
};
} else if (/^\/projects\/derby\/[^/]+$/.test(path) && content.race) {
const race = content.race;
page = {
title: `${race.short} · Derby Lens`,
description: `Estimate odds for the ${race.short}, a ${race.distance} m ${race.discipline} race.`
};
}
return {
title: page ? formatTitle(page.title, environment) : getTitle(path, environment),
description: page?.description ?? 'Stan Runge’s personal website.',
url,
indexable: !!page && !environment && !/^\/blog\/[^/]+\/edit$/.test(path),
type,
image: `${SITE_ORIGIN}/moon.jpg`,
imageAlt: 'Moon icon for Stan Runge’s website',
modifiedAt
};
}