From bce01a013a5d31f38c55737e2b467651fc4067aa Mon Sep 17 00:00:00 2001
From: Eric Wagoner
Date: Wed, 23 Sep 2026 21:04:28 -0400
Subject: [PATCH] The family list knows the hall: about panels link to
gamehall.kestrelsnest.social, and a new game gets a card there
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
---
CONVENTIONS.md | 5 +++++
README.md | 16 +++++++++++-----
family.json | 1 +
new-game.sh | 7 ++++++-
template/src/lib/components/Hall.svelte | 10 +++++++---
template/src/lib/family.ts | 19 +++++++++++++------
template/static/family.json | 9 +++++++++
7 files changed, 52 insertions(+), 15 deletions(-)
diff --git a/CONVENTIONS.md b/CONVENTIONS.md
index c111ea7..5e5e543 100644
--- a/CONVENTIONS.md
+++ b/CONVENTIONS.md
@@ -63,6 +63,11 @@ README.
- **Send word** is the contact paragraph: mail, Bluesky and Mastodon, and
where the keeper roosts. It lives in the about panel; the colophons, the
rules page and the guide each carry a one-line mail link.
+- **The family list** in the about panel, *More games in Kestrel's Hall*,
+ comes from `family.json` and its heading links to the family's landing
+ page, gamehall.kestrelsnest.social. A new game is added to the kit's
+ `family.json` and given a card on that page in the blog repo; both are
+ hand-kept.
- **The hosting line** follows the family list in the about panel: the
games are free and will stay free, the servers cost a few dollars a
month, and ko-fi.com/kestrelsnest is where anyone may chip in. It says
diff --git a/README.md b/README.md
index 1a77b6e..d9245f2 100644
--- a/README.md
+++ b/README.md
@@ -71,8 +71,14 @@ interruptions, Wiz-War's shape, does not fit and keeps its own server.
## The family
`family.json` lists every game of Kestrel's Hall: slug, name, address,
-one-line pitch, player count and origin. Each game ships a copy as
-`static/family.json` (the deploy script takes the kit's latest when the kit
-is beside the game) and its about panel lists the others under *More games
-in Kestrel's Hall*. Add a game here, then deploy the games; nothing
-else needs to know.
+one-line pitch, player count and origin, plus `hall`, the address of the
+family's landing page. Each game ships a copy as `static/family.json` (the
+deploy script takes the kit's latest when the kit is beside the game) and
+its about panel lists the others under *More games in Kestrel's Hall*, the
+heading linking to the family's landing page. Add a game here, then
+deploy the games.
+
+That landing page, https://gamehall.kestrelsnest.social, is one static page
+in the blog repo (`~/Sites/blog/static/gamehall/index.html`) with a card
+per game, written by hand, and it deploys with the blog. A new game is not
+in the family until it has its card there too.
diff --git a/family.json b/family.json
index f9e6d33..48572da 100644
--- a/family.json
+++ b/family.json
@@ -1,5 +1,6 @@
{
"family": "Kestrel's Hall",
+ "hall": "https://gamehall.kestrelsnest.social",
"games": [
{
"slug": "wizwar",
diff --git a/new-game.sh b/new-game.sh
index a69a9b1..4905ce1 100755
--- a/new-game.sh
+++ b/new-game.sh
@@ -45,5 +45,10 @@ Next:
4. Create a Sentry project (the Sentry MCP can) and put its DSN and project id
in deploy/$SLUG.service and deploy/sentry-slack-alert.sh.
5. deploy/deploy.sh
-docs/conventions.md is the house style every game follows.
+ 6. Put the game in the family: add it to the kit's family.json (every game's
+ about panel lists the others from that file at its next deploy), then give
+ it a card on the family's landing page, Kestrel's Hall, hand-written HTML in
+ the blog repo at ~/Sites/blog/static/gamehall/index.html, and deploy the blog.
+ Card art is the game's own drawing; never a publisher's logo or scan.
+CONVENTIONS.md in the kit is the house style every game follows.
MSG
diff --git a/template/src/lib/components/Hall.svelte b/template/src/lib/components/Hall.svelte
index 61a9fff..7758164 100644
--- a/template/src/lib/components/Hall.svelte
+++ b/template/src/lib/components/Hall.svelte
@@ -3,7 +3,7 @@
// bot, open a table, join by code, the ledger of your tables, and the
// about panel with the rules, the guide and a way to reach the keeper.
import { goto } from '$app/navigation';
- import { otherGames, type FamilyGame } from '$lib/family';
+ import { loadFamily, type FamilyGame } from '$lib/family';
import Preferences from './Preferences.svelte';
import { prefs, setPref } from '$lib/prefs.svelte';
import { allSeats, doubtSeat, forgetSeat, rememberSeat, ServerError, trustSeat } from '$lib/net/client';
@@ -33,8 +33,12 @@
const hours = (minutes: number) => (minutes < 90 ? `${minutes} minutes` : `${Math.round(minutes / 60)} hours`);
/** The other games of Kestrel's Hall, for the about panel. */
let family = $state([]);
+ let hall = $state('');
$effect(() => {
- void otherGames('__SLUG__').then((g) => (family = g));
+ void loadFamily('__SLUG__').then((f) => {
+ family = f.games;
+ hall = f.hall;
+ });
});
/** Reports this browser's seats have filed, with the keeper's replies. */
let reports = $state([]);
@@ -282,7 +286,7 @@
{/if}
{#if family.length}
- More games in Kestrel's Hall
+ More games in {#if hall}Kestrel's Hall{:else}Kestrel's Hall{/if}
{#each family as g (g.slug)}
- {g.name}: {g.pitch} {g.players} players; {g.origin}.
diff --git a/template/src/lib/family.ts b/template/src/lib/family.ts
index b706a6b..596072d 100644
--- a/template/src/lib/family.ts
+++ b/template/src/lib/family.ts
@@ -1,6 +1,7 @@
// The other games of Kestrel's Hall, from the family list the kit keeps
// (family.json, one copy shipped with every game), so a player who liked one
-// table can find the next. This game is left out of its own list.
+// table can find the next, and the hall's front door. This game is left out
+// of its own list.
export interface FamilyGame {
slug: string;
@@ -11,13 +12,19 @@ export interface FamilyGame {
origin: string;
}
-export async function otherGames(self: string): Promise {
+/** The family list: the hall's front door and every game but this one. */
+export interface Family {
+ hall: string;
+ games: FamilyGame[];
+}
+
+export async function loadFamily(self: string): Promise {
try {
const res = await fetch('/family.json', { cache: 'no-cache' });
- if (!res.ok) return [];
- const data = (await res.json()) as { games?: FamilyGame[] };
- return (data.games ?? []).filter((g) => g.slug !== self);
+ if (!res.ok) return { hall: '', games: [] };
+ const data = (await res.json()) as { hall?: string; games?: FamilyGame[] };
+ return { hall: data.hall ?? '', games: (data.games ?? []).filter((g) => g.slug !== self) };
} catch {
- return [];
+ return { hall: '', games: [] };
}
}
diff --git a/template/static/family.json b/template/static/family.json
index 2e69a05..48572da 100644
--- a/template/static/family.json
+++ b/template/static/family.json
@@ -1,5 +1,6 @@
{
"family": "Kestrel's Hall",
+ "hall": "https://gamehall.kestrelsnest.social",
"games": [
{
"slug": "wizwar",
@@ -16,6 +17,14 @@
"pitch": "Duelling wizards. Two hands each. The right run of gestures is a spell.",
"players": "2 to 12",
"origin": "Richard Bartle's pencil-and-paper duel, 1977"
+ },
+ {
+ "slug": "hnefatafl",
+ "name": "Hnefatafl",
+ "url": "https://tafl.kestrelsnest.social",
+ "pitch": "The Viking board game. One king, one throne, four corners, and a ring of attackers closing in.",
+ "players": "2",
+ "origin": "the Norse tafl game, reconstructed from Linnaeus's 1732 notes; Copenhagen rules or Tablut"
}
]
}