Start the rebuild: note schema, vault kit and data migration

Phase 1 of the functional design plan.

- Move the old site into legacy/, unchanged, for reference. It still
  builds from there until cutover.
- Add packages/schema: the one definition of all 20 note types, with
  field titles and descriptions, note and vault checks, and JSON Schema
  export so later editing tools and the MCP server share it.
- Add vault-kit: Obsidian templates generated from the schema, and the
  Site guide for 24.01_PersonalWebsite.
- Add tools/migrate: turns the legacy JSON, collections, store data and
  page text into 545 Site folder notes and 84 images, and checks the
  result with the schema.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-11 18:40:55 +00:00
parent 4ded4b277b
commit a2f723ca2d
324 changed files with 11538 additions and 7556 deletions
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env node
/**
* fetch-box-art.mjs
* ---------------------------------------------------------------------------
* Support tool for the Gaming Hall of Fame. Downloads box/banner art for a
* game from the RAWG API (https://rawg.io/apidocs) and saves it into
* `public/images/games/<slug>.jpg`, then prints the public path to paste into
* a game file's `art:` field.
*
* RAWG covers Steam, PlayStation, Xbox, and Nintendo titles in one place, so
* this works regardless of platform. Note: RAWG serves landscape banner /
* screenshot art (not portrait Steam capsules), which matches the landscape
* Hall of Fame cards (aspect-[4/3]).
*
* PREREQUISITES
* 1. Get a free RAWG API key: https://rawg.io/login?forward=developer
* 2. Put it in a `.env` file at the project root:
* RAWG_API_KEY=your_key_here
* (`.env` is gitignored; `.env.example` is the template.)
*
* USAGE
* node scripts/fetch-box-art.mjs "Elden Ring"
* node scripts/fetch-box-art.mjs "Hades" --platform pc
* node scripts/fetch-box-art.mjs "Celeste" --out celeste.jpg
* node scripts/fetch-box-art.mjs --id 3328 # by RAWG id
*
* Run with `--env-file=.env` to auto-load the key (Node 20.6+):
* node --env-file=.env scripts/fetch-box-art.mjs "Elden Ring"
*
* If you don't use --env-file, export RAWG_API_KEY in your shell first.
*/
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, extname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolve(__dirname, '..');
const OUT_DIR = join(PROJECT_ROOT, 'public', 'images', 'games');
const RAWG_BASE = 'https://api.rawg.io/api';
// RAWG parent platforms that map to the --platform flag values.
const PLATFORM_IDS = {
pc: 1,
playstation: 2,
xbox: 3,
nintendo: 7,
mac: 4,
linux: 5,
ios: 5,
android: 6,
};
// --- arg parsing ------------------------------------------------------------
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
console.log(`Usage: node scripts/fetch-box-art.mjs "<game title>" [options]
Options:
--platform <p> narrow search to a parent platform: pc, playstation, xbox, nintendo
--id <rawgId> fetch a specific game by its RAWG id (skips search)
--out <name> output filename (default: slug of the title, .jpg)
-h, --help show this help
Examples:
node scripts/fetch-box-art.mjs "Elden Ring"
node scripts/fetch-box-art.mjs "Hollow Knight" --platform pc
node scripts/fetch-box-art.mjs --id 3328
Tip: run with \`node --env-file=.env scripts/fetch-box-art.mjs ...\` to auto-load
the RAWG_API_KEY from your .env file.`);
process.exit(args.length === 0 ? 1 : 0);
}
const positional = [];
let platform = null;
let rawgId = null;
let outName = null;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--platform') platform = args[++i];
else if (a === '--id') rawgId = Number(args[++i]);
else if (a === '--out') outName = args[++i];
else if (!a.startsWith('--')) positional.push(a);
}
const title = positional.join(' ').trim();
// --- helpers ----------------------------------------------------------------
const slugify = (s) =>
s
.toLowerCase()
.replace(/['’.:]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const fail = (msg) => {
console.error(`✖ ${msg}`);
process.exit(1);
};
const buildImageUrl = (game) => {
// Prefer the wide/landscape screenshot-style image, which suits the
// aspect-[4/3] Hall of Fame cards. Fall back through RAWG's image fields.
const candidates = [
game.background_image,
game.background_image_additional,
];
const url = candidates.find(Boolean);
if (!url) return null;
// RAWG serves http(s) images; force https.
return url.replace(/^http:\/\//i, 'https://');
};
// --- main -------------------------------------------------------------------
const API_KEY = process.env.RAWG_API_KEY;
if (!API_KEY) {
fail(
'RAWG_API_KEY is not set. Add it to .env (see .env.example) and run with\n' +
' node --env-file=.env scripts/fetch-box-art.mjs "..."',
);
}
let game;
try {
if (rawgId) {
console.log(`→ Fetching RAWG game id ${rawgId}…`);
const res = await fetch(`${RAWG_BASE}/games/${rawgId}?key=${API_KEY}`);
if (!res.ok) {
if (res.status === 404) fail(`No RAWG game with id ${rawgId}.`);
fail(`RAWG detail request failed: ${res.status} ${res.statusText}`);
}
game = await res.json();
} else {
if (!title) fail('No game title provided.');
const params = new URLSearchParams({ key: API_KEY, search: title, page_size: '5' });
if (platform) {
const pid = PLATFORM_IDS[platform.toLowerCase()];
if (!pid) {
fail(
`Unknown --platform "${platform}". Try one of: ${Object.keys(PLATFORM_IDS).join(', ')}`,
);
}
params.set('parent_platforms', String(pid));
}
console.log(`→ Searching RAWG for "${title}"${platform ? ` on ${platform}` : ''}…`);
const res = await fetch(`${RAWG_BASE}/games?${params}`);
if (!res.ok) fail(`RAWG search failed: ${res.status} ${res.statusText}`);
const data = await res.json();
if (!data.results?.length) fail(`No RAWG results for "${title}".`);
game = data.results[0];
console.log(` found: "${game.name}" (id ${game.id})${game.released ? `, ${game.released.slice(0, 4)}` : ''}`);
}
} catch (err) {
fail(err?.message ? `Network error: ${err.message}` : 'Network error contacting RAWG.');
}
const imageUrl = buildImageUrl(game);
if (!imageUrl) {
fail(`RAWG has no usable image for "${game.name}". Try another title or --id.`);
}
// Decide output filename + extension (keep the source extension if --out omits one).
const baseName = outName ?? `${slugify(game.name)}.jpg`;
const ext = extname(baseName) || '.jpg';
const fileName = baseName.replace(/\.[^.]+$/, '') + ext;
const outPath = join(OUT_DIR, fileName);
console.log(`→ Downloading ${imageUrl}`);
const imgRes = await fetch(imageUrl);
if (!imgRes.ok) fail(`Image download failed: ${imgRes.status} ${imgRes.statusText}`);
const buf = Buffer.from(await imgRes.arrayBuffer());
await mkdir(OUT_DIR, { recursive: true });
await writeFile(outPath, buf);
const publicPath = `/images/games/${fileName}`;
console.log(`\n✔ Saved: ${outPath}`);
console.log(` Use this in your game file's frontmatter:`);
console.log(` art: "${publicPath}"`);