diff --git a/site/CLAUDE.md b/site/CLAUDE.md
index e6e05b1..00c0061 100644
--- a/site/CLAUDE.md
+++ b/site/CLAUDE.md
@@ -34,10 +34,13 @@ client component.
`scripts/generate-llms-txt.mjs`.
- `src/app/page.tsx`: server-rendered landing page.
- `src/app/_components/`: `SkillCard` (server), `icons` (server, inline
- SVGs), `CopyButton` / `SkillsFilter` / `ThemeSwitchIsland` (client
- islands). `SkillsFilter` is used twice — once for the skills grid
- (`All` + categories) and once for the Installing section's tabs
- (Claude Code / Cursor / npx skills / Clone repo).
+ SVGs), `CopyButton` / `SkillsFilter` / `CommunitySearch` /
+ `ThemeSwitchIsland` (client islands). `SkillsFilter` is used twice — once
+ for the skills grid (`All` + categories) and once for the Installing
+ section's tabs (Claude Code / Cursor / npx skills / Clone repo).
+ `CommunitySearch` adds search + pagination to the community grid; the
+ cards render server-side and are passed in as children, so every entry
+ stays in the static HTML.
- `scripts/copy-skills.mjs`: mirrors `../skills/` into
`public/` on `predev` / `prebuild`. No network; just `cp`.
- `scripts/generate-llms-txt.mjs`: writes `public/llms.txt` from
@@ -80,7 +83,9 @@ then append to `SKILL_CARD_SOURCES`:
`pnpm sync:skills && pnpm dev` to verify. New category? Add it to the
`FilterType` union and the `FILTERS` array.
-**Ecosystem:** external link, no upstream copy.
+**Ecosystem:** external link, no upstream copy. New entries are picked
+up automatically by the community search, pagination, and llms.txt; no
+other wiring needed.
```ts
{
@@ -88,7 +93,6 @@ then append to `SKILL_CARD_SOURCES`:
description: "Verb-led summary of what the skill does.",
pathLabel: "owner/repo",
copyValue: "https://github.com/owner/repo/blob/main/path/to/SKILL.md",
- category: "Ecosystem",
}
```
diff --git a/site/README.md b/site/README.md
index 13506f5..4a97fdc 100644
--- a/site/README.md
+++ b/site/README.md
@@ -151,7 +151,6 @@ Append to `ECOSYSTEM_CARDS` in `src/data/skills.ts`:
description: "Verb-led summary of what the skill does.",
pathLabel: "owner/repo",
copyValue: "https://github.com/owner/repo/blob/main/path/to/SKILL.md",
- category: "Ecosystem",
}
```
@@ -160,7 +159,6 @@ Append to `ECOSYSTEM_CARDS` in `src/data/skills.ts`:
- `copyValue` is the full URL written to the clipboard when the user
clicks the pill — point it directly at the raw SKILL.md so an agent
can fetch it.
-- `category` is always `"Ecosystem"` for this section.
Run `pnpm dev` to verify the card renders. No `sync:skills` step
needed since ecosystem entries aren't mirrored locally.
diff --git a/site/scripts/generate-llms-txt.mjs b/site/scripts/generate-llms-txt.mjs
index bd7afd0..a822c54 100644
--- a/site/scripts/generate-llms-txt.mjs
+++ b/site/scripts/generate-llms-txt.mjs
@@ -53,7 +53,6 @@ const ecosystemCards = ECOSYSTEM_CARDS.map((c) => ({
title: c.title,
description: c.description,
copyValue: c.copyValue,
- category: c.category,
}));
const filters = [...FILTERS];
@@ -137,7 +136,7 @@ if (realEcosystem.length > 0) {
lines.push("## Community Built");
lines.push("");
lines.push(
- "Other community-built skills that may be helpful for your build. These aren't installed via the methods above; each project has its own setup, so follow the link on each entry. Not endorsed by the Stellar Foundation; do your own research.",
+ "Other community-built skills that may be helpful for your build. These aren't installed via the methods above; each project has its own setup, so follow the link on each entry. Not endorsed by the Stellar Foundation; do your own research. To get a skill listed here, open a pull request adding it to ECOSYSTEM_CARDS in site/src/data/skills.ts of https://github.com/stellar/stellar-dev-skill.",
);
lines.push("");
for (const c of realEcosystem) {
diff --git a/site/src/app/_components/CommunitySearch.tsx b/site/src/app/_components/CommunitySearch.tsx
new file mode 100644
index 0000000..d6fdaaa
--- /dev/null
+++ b/site/src/app/_components/CommunitySearch.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { Children, ReactNode, useId, useState } from "react";
+
+import { Input, Pagination } from "@stellar/design-system";
+
+/** Cards shown per page in the 2-column community grid. */
+const PAGE_SIZE = 8;
+
+type Props = {
+ /** Lowercased "title description" haystack for each child, in the
+ * same render order as `children`. Search runs against these strings
+ * so the cards themselves stay server-rendered; this island only
+ * toggles wrapper visibility. */
+ searchTexts: readonly string[];
+ /** Pre-rendered card markup (server). One child per searchTexts
+ * entry, same order. */
+ children: ReactNode;
+};
+
+/**
+ * Client island that adds search and pagination to the community
+ * skills grid. The cards render server-side and are passed in as
+ * `children` (same slot pattern as `SkillsFilter`), so every card's
+ * title/description/link stays in the static HTML for non-JS clients;
+ * this component only hides wrappers that don't match the query or
+ * fall outside the current page.
+ *
+ * Search matches a case-insensitive substring of the card title or any
+ * part of its description, so a query like "DEX" finds a card that
+ * only mentions it mid-description.
+ */
+export const CommunitySearch = ({ searchTexts, children }: Props) => {
+ const searchInputId = useId();
+ const [query, setQuery] = useState("");
+ const [page, setPage] = useState(1);
+
+ const needle = query.trim().toLowerCase();
+ const matches = searchTexts.reduce((acc, text, index) => {
+ if (text.includes(needle)) acc.push(index);
+ return acc;
+ }, []);
+
+ // Clamp instead of trusting `page`: a new query can shrink the match
+ // list below the previously selected page.
+ const pageCount = Math.max(1, Math.ceil(matches.length / PAGE_SIZE));
+ const currentPage = Math.min(page, pageCount);
+ const visible = new Set(
+ matches.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE),
+ );
+
+ return (
+ <>
+