Files
website/src/lib/seo.ts
Stan Runge afb1f191df
All checks were successful
Test and deploy staging / staging (push) Successful in 1m49s
add world opinions project
World map with switchable layers (what "liberal" means, what left vs right
is about, traditional/secular and survival/self-expression values), country
cards, a shared-scale party compass, a values scatter and national
narratives of major events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:24:40 +00:00

124 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.'
},
'/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 (/^\/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
};
}