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 }; /** Pages that describe themselves, such as language learner lessons. */ type Described = { title: string; description: string; index?: boolean }; type PageContent = { blogArticle?: Article; list?: List; race?: Race; seo?: Described }; type Seo = { title: string; description: string; url: string; indexable: boolean; type: 'website' | 'article'; image: string; imageAlt: string; modifiedAt?: string; }; const publicPages: Record = { '/': { title: 'Projects, lists and writing', description: 'Stan Runge’s projects, lists, writing and experience.' }, '/projects': { title: 'Projects', description: 'Software projects by Stan Runge, including osu! map finder, Derby Lens and Language Learner.' }, '/blog': { title: 'Blog', description: 'Articles and notes by Stan Runge.' }, '/trackers': { title: 'Trackers', description: 'Stan Runge’s trackers: games, money, houses, Counter-Strike prices, Animaldex and lists.' }, '/lists': { title: 'Lists', description: 'Lists collected and maintained by Stan Runge.' }, '/projects/game-tracker': { title: 'Game Tracker', description: 'Pokémon adventures and collections, Call of Duty ranks and unlocks, and osu! scores and training.' }, '/projects/game-tracker/pokemon': { title: 'Pokémon journal', description: 'Explore Pokémon encounters and regional Pokédexes, record catches and teams, and track badges across your playthroughs.' }, '/projects/game-tracker/osu': { title: 'osu! training', description: 'Keep memorable scores, beatmap goals and a training journal for speed, stamina and consistency.' }, '/projects/game-tracker/pokemon/locations': { title: 'Pokémon locations', description: 'Every Pokémon location by region, with the Pokémon you can find there.' }, '/projects/animaldex': { title: 'Animaldex', description: 'Find and add neighborhood animals on a map.' }, '/projects/cs-prices': { title: 'CS Prices', description: 'Compare historical Counter-Strike market prices by listing, weapon, skin, wear, and other item dimensions.' }, '/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.' }, '/projects/language-learner': { title: 'Language Learner', description: 'Learn Dutch, Korean and English from beginner to advanced, with lessons and endless daily challenges.' }, '/projects/world-opinions': { title: 'World Opinions', description: 'How political words, parties, values and national histories differ across the world.' } }; 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 (content.seo) { page = content.seo; } 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) && content.seo?.index !== false, type, image: `${SITE_ORIGIN}/moon.jpg`, imageAlt: 'Moon icon for Stan Runge’s website', modifiedAt }; }