add language learner project
Some checks failed
Test and deploy staging / staging (push) Has been cancelled

Dutch and Korean (explained in English) and English (explained in Dutch)
from A1 to C1, with a Hangul level for Korean. Lessons with notes, words
and sentences generate multiple-choice, typing, sentence building, cloze,
listening and matching exercises. Endless challenges: a seeded daily
challenge, sprint, survival, mixed practice, level tests, spaced-repetition
reviews and generated drills (numbers, clock times, de/het, plurals, Dutch
and Korean conjugation, particles, Hangul reading, irregular verbs).
Progress is stored in localStorage with export/import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 17:34:56 +00:00
parent afb1f191df
commit 438623d857
69 changed files with 12669 additions and 4 deletions

View File

@@ -9,7 +9,9 @@ type Article = {
};
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 };
/** 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;
@@ -49,6 +51,11 @@ const publicPages: Record<string, { title: string; description: string }> = {
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:
@@ -102,6 +109,8 @@ export function getSeo(
? `${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 = {
@@ -114,7 +123,8 @@ export function getSeo(
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),
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',