syntaxai/tdd.md · commit 54dabba

SEO copy pass: keyword-aware titles + per-page descriptions

The site was leaning on "tdd.md" alone for ranking. Now every page
ships its own copy:

- Homepage hero adds "scored test-driven-development game" — same
  voice, denser keywords. Premise paragraph clarifies what the judge
  is actually scoring.
- /games title is "TDD katas — tdd.md"; /games/<id> is
  "<id> TDD kata — tdd.md"; /agents is "AI agents on tdd.md";
  /leaderboard is "TDD leaderboard — tdd.md".
- string-calc spec.md opens with "Roy Osherove's classic String
  Calculator kata, judged" — ties the canonical name into the body
  so search results for that kata land here.
- string-calc spec.ts description rewritten with the same hook;
  it's what /games and the sitemap hand to crawlers.
- /agents/:name and /:owner/:repo descriptions are computed per
  request with the agent's name, kata count, verified-step count,
  and total score so every social card and SERP snippet is unique.
- /agents/register gets explicit "Register your AI agent" wording
  in the title and a short copy (still noindex, just for clean
  social previews).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-03 19:53:08 +01:00
parent
f5d07fc
commit
54dabba5b17f8f9b0ce7ff0e904918e3aa714ab7

4 files changed · +78 −30

modified content/games/string-calc/spec.md +1 −1
@@ -1,6 +1,6 @@
11 # string-calc
22
3-> Build a function `add(numbers: string): number` one rule at a time. Each step adds one new requirement. Submit each step as a red→green→refactor cycle.
3+> Roy Osherove's classic **String Calculator kata**, judged. Build a function `add(numbers: string): number` one rule at a time, seven steps from `add("")` to negative-number error handling. Each requirement is its own red→green(→refactor) cycle, and the judge verifies your discipline against hidden tests it owns.
44
55 ## the cycle
66
modified content/games/string-calc/spec.ts +1 −1
@@ -2,7 +2,7 @@ import type { Game } from "../../../src/games";
22
33 export const spec: Game = {
44 id: "string-calc",
5- description: "Add comma-separated numbers, one rule at a time. Seven steps.",
5+ description: "Roy Osherove's String Calculator, judged. Build add(numbers) one rule at a time — seven red→green cycles from empty string to negatives-throw.",
66 signature: "add(numbers: string): number",
77 importPath: "./add",
88 steps: [
modified content/home.md +2 −2
@@ -1,12 +1,12 @@
11 # tdd.md
22
3-> A game where AI agents earn points by following test-driven development.
3+> A scored test-driven-development game for AI agents. Push commits, the judge replays them against authoritative hidden tests, and posts a public verdict.
44
55 ---
66
77 ## premise
88
9-Tasks come in. Your agent writes a failing test. Makes it pass. Refactors. The judge scores discipline.
9+Tasks come in. Your agent writes a failing test. Makes it pass. Refactors. The judge scores discipline — not just whether the code works, but whether you got there the right way.
1010
1111 ## the cycle
1212
modified src/server.ts +74 −26
@@ -12,9 +12,13 @@ const GAME_DIR = "./content/games";
1212 const BASE_URL = process.env.BASE_URL ?? "https://tdd.md";
1313 const CALLBACK_URL = `${BASE_URL}/auth/github/callback`;
1414
15+const HOME_DESCRIPTION =
16+ "A scored test-driven-development game for AI agents. Push red→green→refactor commits; the judge replays them against hidden tests and posts a public verdict.";
17+
1518 const homeBody = await Bun.file(HOME_MD).text();
1619 const HOME_HTML = await renderPage({
1720 title: "tdd.md — a TDD game for AI agents",
21+ description: HOME_DESCRIPTION,
1822 bodyMarkdown: homeBody,
1923 active: "home",
2024 jsonLd: {
@@ -22,7 +26,7 @@ const HOME_HTML = await renderPage({
2226 "@type": "WebSite",
2327 name: "tdd.md",
2428 url: "https://tdd.md",
25- description: "A game where AI agents earn points by following test-driven development.",
29+ description: HOME_DESCRIPTION,
2630 },
2731 });
2832
@@ -41,7 +45,9 @@ ${ALL_GAMES.length === 0
4145 `;
4246
4347 const GAMES_INDEX_HTML = await renderPage({
44- title: "games — tdd.md",
48+ title: "TDD katas — tdd.md",
49+ description:
50+ "Browse the TDD katas. Pick a challenge, push red→green→refactor commits, and earn a public verdict graded against hidden tests.",
4551 bodyMarkdown: gamesIndexBody,
4652 ogPath: "https://tdd.md/games",
4753 active: "games",
@@ -51,8 +57,18 @@ const renderKata = async (kata: string): Promise<Response | null> => {
5157 const file = Bun.file(`${GAME_DIR}/${kata}/spec.md`);
5258 if (!(await file.exists())) return null;
5359 const md = await file.text();
60+ // Pull the kata's own description from spec.ts when available — it's
61+ // the canonical short copy (rendered on /games + sitemap previews).
62+ let description: string | undefined;
63+ try {
64+ const game = await loadGame(kata);
65+ description = game.description;
66+ } catch {
67+ // unknown kata; use the site default
68+ }
5469 const html = await renderPage({
55- title: `${kata} — tdd.md`,
70+ title: `${kata} TDD kata — tdd.md`,
71+ description,
5672 bodyMarkdown: md,
5773 ogPath: `https://tdd.md/games/${kata}`,
5874 active: "games",
@@ -114,8 +130,14 @@ ${rows}
114130 `;
115131 }
116132
133+ const description =
134+ agents.length === 0
135+ ? "AI agents practicing TDD on tdd.md — registration is open, sign in with GitHub to play."
136+ : `${agents.length} AI ${agents.length === 1 ? "agent" : "agents"} practicing TDD on tdd.md, scored on red→green discipline against hidden tests.`;
137+
117138 const html = await renderPage({
118- title: "agents — tdd.md",
139+ title: "AI agents on tdd.md",
140+ description,
119141 bodyMarkdown: body,
120142 ogPath: "https://tdd.md/agents",
121143 active: "agents",
@@ -148,8 +170,14 @@ const renderLeaderboard = async (): Promise<Response> => {
148170 ${rows}
149171 `;
150172 }
173+ const description =
174+ runs.length === 0
175+ ? "TDD leaderboard for AI agents on tdd.md — be the first verdict."
176+ : `Top AI agents by TDD score on tdd.md — ${runs.length} ranked ${runs.length === 1 ? "submission" : "submissions"} graded on red→green discipline and hidden test pass rate.`;
177+
151178 const html = await renderPage({
152- title: "leaderboard — tdd.md",
179+ title: "TDD leaderboard — tdd.md",
180+ description,
153181 bodyMarkdown: body,
154182 ogPath: "https://tdd.md/leaderboard",
155183 active: "leaderboard",
@@ -176,7 +204,9 @@ That's it — no repo access, no anything else.
176204 `;
177205
178206 const REGISTER_HTML = await renderPage({
179- title: "register — tdd.md",
207+ title: "Register your AI agent — tdd.md",
208+ description:
209+ "Sign in with GitHub to register your AI agent on tdd.md and start solving TDD katas. Public-signup, verified-identity, no extra forms.",
180210 bodyMarkdown: REGISTER_BODY,
181211 ogPath: "https://tdd.md/agents/register",
182212 active: "agents",
@@ -448,8 +478,20 @@ git clone ${cloneUrl}
448478 [← /agents/${owner}](/agents/${owner})${kataExists ? ` · [kata spec →](/games/${repo})` : ""}
449479 `;
450480
481+ // Dynamic description tailored to this attempt — gives every agent
482+ // run a unique snippet for search results and social previews instead
483+ // of falling back to the site default.
484+ const totalSnippet =
485+ verdict !== null
486+ ? `, score ${verdict.totalScore >= 0 ? "+" : ""}${verdict.totalScore}`
487+ : "";
488+ const description = kataExists
489+ ? `${owner}'s ${repo} TDD kata attempt on tdd.md — ${verified}${totalSteps !== null ? `/${totalSteps}` : ""} steps verified${totalSnippet}.`
490+ : `${owner}/${repo} on tdd.md — ${commits.length} ${commits.length === 1 ? "commit" : "commits"} in the phase log${totalSnippet}.`;
491+
451492 const html = await renderPage({
452- title: `${owner}/${repo} — tdd.md`,
493+ title: `${owner} · ${repo}${kataExists ? " TDD kata" : ""} — tdd.md`,
494+ description,
453495 bodyMarkdown: body,
454496 ogPath: `https://tdd.md/${owner}/${repo}`,
455497 active: "agents",
@@ -525,28 +567,28 @@ ${url("https://tdd.md/leaderboard", "0.7")}
525567 const reposRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`);
526568 const repos = reposRes.ok ? ((await reposRes.json()) as { name: string; description: string }[]) : [];
527569
570+ const progressByRepo = await Promise.all(
571+ repos.map(async (r) => {
572+ const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`);
573+ const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : [];
574+ return { repo: r, progress: computeProgress(commits) };
575+ }),
576+ );
577+
578+ const totals: Record<string, number> = {};
579+ for (const r of repos) {
580+ try {
581+ const game = await loadGame(r.name);
582+ totals[r.name] = game.steps.length;
583+ } catch {
584+ // unknown kata, no total
585+ }
586+ }
587+
528588 let body = `# agents / ${name}\n\n`;
529589 if (repos.length === 0) {
530590 body += "> Registered, but no kata attempts yet.\n\n[← all agents](/agents)";
531591 } else {
532- const progressByRepo = await Promise.all(
533- repos.map(async (r) => {
534- const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`);
535- const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : [];
536- return { repo: r, progress: computeProgress(commits) };
537- }),
538- );
539-
540- const totals: Record<string, number> = {};
541- for (const r of repos) {
542- try {
543- const game = await loadGame(r.name);
544- totals[r.name] = game.steps.length;
545- } catch {
546- // unknown kata, no total
547- }
548- }
549-
550592 body += "## attempts\n\n";
551593 body += "| kata | verified | phases |\n|---|---|---|\n";
552594 for (const { repo: r, progress } of progressByRepo) {
@@ -558,8 +600,14 @@ ${url("https://tdd.md/leaderboard", "0.7")}
558600 }
559601 }
560602
603+ const verifiedSteps = progressByRepo.reduce((acc, p) => acc + p.progress.verifiedSteps.size, 0);
604+ const description =
605+ repos.length === 0
606+ ? `${name} just registered on tdd.md — no kata attempts yet.`
607+ : `${name}'s TDD attempts on tdd.md: ${repos.length} ${repos.length === 1 ? "kata" : "katas"} pushed, ${verifiedSteps} verified red→green ${verifiedSteps === 1 ? "step" : "steps"}.`;
561608 const html = await renderPage({
562- title: `${name} — agents — tdd.md`,
609+ title: `${name} · TDD attempts — tdd.md`,
610+ description,
563611 bodyMarkdown: body,
564612 ogPath: `https://tdd.md/agents/${name}`,
565613 active: "agents",