Skip to content

Commit b977b2e

Browse files
hsnice16claude
andcommitted
feat: SEO — keyword-rich metadata + ItemList & SoftwareSourceCode JSON-LD
Centralise APP_KEYWORDS in lib/version.ts (agent names, AGENTS.md / CLAUDE.md, host names, intent variants) and surface them at three layers: - root layout pulls from APP_KEYWORDS so every page inherits the vocabulary - home page gains a long-form title/description and an ItemList JSON-LD listing all ranked repos so each /repo/{id} URL is reachable to crawlers from / - repo detail page emits per-repo keywords (slug, owner, language, every agent label paired with the repo name) and adds a SoftwareSourceCode entity alongside the existing breadcrumb so each page ranks for the project's name AGENTS.md, code-review and quality-check skills updated to add app/page.tsx to the JSON-LD allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e6092e9 commit b977b2e

7 files changed

Lines changed: 122 additions & 34 deletions

File tree

.claude/skills/code-review/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Only `@phosphor-icons/react`. Block Lucide, Heroicons, React Icons, inline SVG,
5454
## Security
5555

5656
- Parameterised SQL only.
57-
- `dangerouslySetInnerHTML` is allowed only for the existing server-built JSON-LD scripts (`app/layout.tsx`, `app/repo/[id]/page.tsx`, `app/package/[registry]/[name]/page.tsx`) with the `<``<` escape preserved. Reject any new use.
57+
- `dangerouslySetInnerHTML` is allowed only for the existing server-built JSON-LD scripts (`app/layout.tsx`, `app/page.tsx`, `app/repo/[id]/page.tsx`, `app/package/[registry]/[name]/page.tsx`) with the `<``<` escape preserved. Reject any new use.
5858
- External links include `rel="noopener noreferrer"`.
5959
- Never execute code from a cloned repo.
6060

.claude/skills/quality-check/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Run the four checks below on any diff affecting UI or I/O. Report findings group
3939
## Security
4040

4141
- **SQL parameterisation**: every query uses `?` placeholders. No string concatenation.
42-
- **`dangerouslySetInnerHTML`** is allowed only for server-built JSON-LD (`app/layout.tsx`, `app/repo/[id]/page.tsx`, `app/package/[registry]/[name]/page.tsx`) and must keep the `<``<` escape. Any other use must be rejected.
42+
- **`dangerouslySetInnerHTML`** is allowed only for server-built JSON-LD (`app/layout.tsx`, `app/page.tsx`, `app/repo/[id]/page.tsx`, `app/package/[registry]/[name]/page.tsx`) and must keep the `<``<` escape. Any other use must be rejected.
4343
- **External URLs** in `<a target="_blank">` always include `rel="noopener noreferrer"`.
4444
- **User input at every boundary** is validated: `parseRepoUrl` for repo URLs, `Number.isFinite` for numeric params, length caps on search strings.
4545
- **Clone safety**: `git clone --depth 1 --single-branch`; never execute code from a clone (no `bun install`, no `npm install`, no post-clone scripts).

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Hooks docs: <https://docs.claude.com/en/docs/claude-code/hooks.html>.
176176

177177
- We `git clone --depth 1 --single-branch` arbitrary URLs — safe by default. We never run post-clone scripts, never `npm install`, never execute code from the clone.
178178
- SQL: all queries parameterised. No interpolation.
179-
- HTML: React auto-escapes. The only `dangerouslySetInnerHTML` is server-built JSON-LD with `<` escaped to `<` (`app/layout.tsx`, `app/repo/[id]/page.tsx`, `app/package/[registry]/[name]/page.tsx`); never feed user-controlled strings into it.
179+
- HTML: React auto-escapes. The only `dangerouslySetInnerHTML` is server-built JSON-LD with `<` escaped to `<` (`app/layout.tsx`, `app/page.tsx`, `app/repo/[id]/page.tsx`, `app/package/[registry]/[name]/page.tsx`); never feed user-controlled strings into it.
180180
- Local-path mode reads files; never writes outside `data/` and the clone workspace passed to `shallowClone`.
181181
- No auth yet (read-only dashboard). When auth lands (`tasks/0.6.0/01-opt-out-claim-flow.md`), do it via OAuth and gate DB writes per user.
182182

app/layout.tsx

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { BackToTop } from "@/components/BackToTop";
66
import { GoogleAnalytics } from "@/components/GoogleAnalytics";
77
import { MobileNav } from "@/components/MobileNav";
88
import { VersionPill } from "@/components/VersionPill";
9-
import { APP_DESCRIPTION, APP_NAME, APP_URL, REPO_URL } from "@/lib/version";
9+
import { APP_DESCRIPTION, APP_KEYWORDS, APP_NAME, APP_URL, REPO_URL } from "@/lib/version";
1010

1111
const OG_IMAGE = {
1212
width: 1200,
@@ -16,28 +16,13 @@ const OG_IMAGE = {
1616
};
1717

1818
export const metadata: Metadata = {
19+
keywords: APP_KEYWORDS,
1920
applicationName: APP_NAME,
21+
creator: "Himanshu Singh",
2022
description: APP_DESCRIPTION,
2123
metadataBase: new URL(APP_URL),
22-
title: { default: APP_NAME, template: `%s · ${APP_NAME}` },
23-
keywords: [
24-
"AI coding agents",
25-
"Claude Code",
26-
"Cursor",
27-
"Devin",
28-
"GPT-5 Codex",
29-
"Gemini CLI",
30-
"Aider",
31-
"OpenHands",
32-
"Pi",
33-
"AGENTS.md",
34-
"agent readiness",
35-
"open source",
36-
"repo ranking",
37-
"developer tools",
38-
],
39-
creator: "Himanshu Singh",
4024
authors: [{ name: "Himanshu Singh", url: REPO_URL }],
25+
title: { default: APP_NAME, template: `%s · ${APP_NAME}` },
4126
openGraph: {
4227
url: "/",
4328
locale: "en_US",

app/page.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ArrowUpRight } from "@phosphor-icons/react/dist/ssr";
2+
import type { Metadata } from "next";
23
import { headers } from "next/headers";
34
import Link from "next/link";
45
import { HostPill } from "@/components/HostPill";
@@ -12,9 +13,23 @@ import { SortSelect } from "@/components/SortSelect";
1213
import { type Host, isHost } from "@/lib/constants/hosts";
1314
import { LEADERBOARD_PAGE_SIZE, LEADERBOARD_PAGE_SIZE_MOBILE } from "@/lib/constants/scoring";
1415
import { DEFAULT_DIR, DEFAULT_SORT, isSortDir, isSortKey, type SortDir, type SortKey } from "@/lib/constants/sort";
15-
import { getLeaderboardStats, type LeaderboardRow, listLeaderboard } from "@/lib/db";
16+
import { getLeaderboardStats, type LeaderboardRow, listLeaderboard, listLeaderboardOverall } from "@/lib/db";
1617
import { MODEL_BY_ID, MODELS, type ModelId } from "@/lib/scoring/weights";
1718
import { compactStars, relativeTime } from "@/lib/utils/format";
19+
import { APP_URL } from "@/lib/version";
20+
21+
const HOME_TITLE =
22+
"Agent Friendly Code — AI coding agent friendliness leaderboard for Claude Code, Cursor, Devin, Codex, Gemini, Aider, OpenHands, Pi";
23+
const HOME_DESCRIPTION =
24+
"Public leaderboard ranking GitHub, GitLab, and Bitbucket repos by how agent-friendly they are for Claude Code, Cursor, Devin, GPT-5 Codex, Gemini CLI, Aider, OpenHands, and Pi — per model, with AGENTS.md / CLAUDE.md, CI, tests, and dev-env signals.";
25+
26+
export const metadata: Metadata = {
27+
title: HOME_TITLE,
28+
description: HOME_DESCRIPTION,
29+
alternates: { canonical: "/" },
30+
twitter: { title: HOME_TITLE, description: HOME_DESCRIPTION },
31+
openGraph: { title: HOME_TITLE, description: HOME_DESCRIPTION, url: "/" },
32+
};
1833

1934
type SearchParams = {
2035
q?: string;
@@ -103,8 +118,29 @@ export default async function Page({ searchParams }: { searchParams: Promise<Sea
103118
? "Simple average of every per-model score."
104119
: (MODELS.find((m) => m.id === selected)?.rationale ?? "");
105120

121+
const allOverall = listLeaderboardOverall();
122+
const itemListJsonLd = {
123+
"@type": "ItemList",
124+
"@context": "https://schema.org",
125+
numberOfItems: allOverall.length,
126+
name: "Agent-friendliness leaderboard",
127+
itemListElement: allOverall.map((row, idx) => ({
128+
"@type": "ListItem",
129+
position: idx + 1,
130+
name: `${row.owner}/${row.name}`,
131+
url: `${APP_URL}/repo/${row.id}`,
132+
})),
133+
};
134+
106135
return (
107136
<>
137+
<script
138+
type="application/ld+json"
139+
// biome-ignore lint/security/noDangerouslySetInnerHtml: server-built JSON-LD; `<` is escaped
140+
dangerouslySetInnerHTML={{
141+
__html: JSON.stringify(itemListJsonLd).replace(/</g, "\\u003c"),
142+
}}
143+
/>
108144
<section className="mb-5">
109145
<h1 className="mb-3 text-[26px] font-bold leading-[1.2] tracking-tight sm:text-[32px] sm:leading-[1.18]">
110146
Which public repos are friendliest to an AI coding agent?

app/repo/[id]/page.tsx

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { SignalRow } from "@/components/SignalRow";
1414
import { STRENGTHS_GAPS_VISIBLE_LIMIT } from "@/lib/constants/scoring";
1515
import { getAlternatives, getModelScores, getRepo, getSignalResults } from "@/lib/db";
1616
import { topImprovements } from "@/lib/scoring/scorer";
17-
import { MODEL_BY_ID, type ModelId } from "@/lib/scoring/weights";
18-
import { APP_URL } from "@/lib/version";
17+
import { MODEL_BY_ID, MODELS, type ModelId } from "@/lib/scoring/weights";
18+
import { APP_KEYWORDS, APP_URL } from "@/lib/version";
1919

2020
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
2121
const { id: idStr } = await params;
@@ -36,9 +36,23 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
3636
const title = `${slug}${score} / 100`;
3737
const description = `Agent-friendliness score for ${slug} across Claude Code, Cursor, Devin, and GPT-5 Codex — with the top improvements ranked by score-gain.`;
3838

39+
const repoKeywords = [
40+
slug,
41+
repo.name,
42+
repo.owner,
43+
repo.language,
44+
`${repo.name} ai agent`,
45+
`${repo.name} agents.md`,
46+
`${repo.name} ai coding agent`,
47+
...MODELS.map((m) => `${repo.name} ${m.label.toLowerCase()}`),
48+
...MODELS.map((m) => m.label),
49+
...APP_KEYWORDS,
50+
].filter((k): k is string => Boolean(k));
51+
3952
return {
4053
title,
4154
description,
55+
keywords: repoKeywords,
4256
twitter: { title, description },
4357
alternates: { canonical: `/repo/${id}` },
4458
openGraph: { title, description, url: `/repo/${id}`, type: "article" },
@@ -79,19 +93,31 @@ export default async function Page({
7993
const slug = `${repo.owner}/${repo.name}`;
8094
const jsonLd = {
8195
"@context": "https://schema.org",
82-
"@type": "BreadcrumbList",
83-
itemListElement: [
96+
"@graph": [
8497
{
85-
"@type": "ListItem",
86-
position: 1,
87-
name: "Leaderboard",
88-
item: `${APP_URL}/`,
98+
"@type": "BreadcrumbList",
99+
itemListElement: [
100+
{
101+
"@type": "ListItem",
102+
position: 1,
103+
item: `${APP_URL}/`,
104+
name: "Leaderboard",
105+
},
106+
{
107+
"@type": "ListItem",
108+
name: slug,
109+
position: 2,
110+
item: `${APP_URL}/repo/${id}`,
111+
},
112+
],
89113
},
90114
{
91-
"@type": "ListItem",
92-
position: 2,
115+
"@type": "SoftwareSourceCode",
93116
name: slug,
94-
item: `${APP_URL}/repo/${id}`,
117+
codeRepository: repo.url,
118+
url: `${APP_URL}/repo/${id}`,
119+
...(repo.language ? { programmingLanguage: repo.language } : {}),
120+
description: `Agent-friendliness score for ${slug} across Claude Code, Cursor, Devin, GPT-5 Codex, Gemini CLI, Aider, OpenHands, and Pi.`,
95121
},
96122
],
97123
};

lib/version.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,44 @@ export const REPO_URL = "https://github.com/hsnice16/agent-friendly-code";
77
export const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "https://agent-friendly-code.vercel.app";
88
export const APP_DESCRIPTION =
99
"Public dashboard ranking open-source repos by how friendly they are to AI coding agents (Claude Code, Cursor, Devin, GPT-5 Codex) — per model, across GitHub, GitLab, and Bitbucket.";
10+
11+
export const APP_KEYWORDS = [
12+
"ai",
13+
"code",
14+
"agent",
15+
"codex",
16+
"devin",
17+
"claude",
18+
"cursor",
19+
"devin ai",
20+
"friendly",
21+
"cursor ai",
22+
"claude code",
23+
"gpt-5 codex",
24+
"openai codex",
25+
"ai code agent",
26+
"agent friendly",
27+
"ai code agents",
28+
"ai coding agent",
29+
"ai coding agents",
30+
"coding assistant",
31+
"agent friendly code",
32+
"ai pair programming",
33+
"ai software engineer",
34+
"pi",
35+
"aider",
36+
"gemini",
37+
"github",
38+
"gitlab",
39+
"bitbucket",
40+
"AGENTS.md",
41+
"CLAUDE.md",
42+
"openhands",
43+
"gemini cli",
44+
"open source",
45+
"repo ranking",
46+
"agent readiness",
47+
"developer tools",
48+
"agent ready repo",
49+
"gemini code assist",
50+
];

0 commit comments

Comments
 (0)