diff --git a/site/CLAUDE.md b/site/CLAUDE.md
index 1cd820e..da31672 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 4909a41..1eaf98d 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 db60a2c..267136a 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];
@@ -150,7 +149,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 (
+ <>
+
diff --git a/site/src/app/styles.scss b/site/src/app/styles.scss
index 79532dd..c66100c 100644
--- a/site/src/app/styles.scss
+++ b/site/src/app/styles.scss
@@ -557,6 +557,107 @@ p.SkillsLanding__sectionDescription {
}
}
+// Community search input (client island; the cards stay server-rendered).
+// Width matches one grid column (50% minus half the 16px column gap) so
+// the input lines up with the card below it.
+.SkillsLanding__communitySearch {
+ width: calc(50% - #{pxToRem(8px)});
+ margin-bottom: pxToRem(16px);
+
+ @media (max-width: 600px) {
+ width: 100%;
+ }
+}
+
+// Each grid cell wraps one server-rendered card. `display: grid` makes
+// the card stretch to the full row height, like a direct grid child.
+.SkillsLanding__communityItem {
+ display: grid;
+}
+
+// Hidden by search/pagination. The card markup stays in the HTML so
+// non-JS clients (AI agents, crawlers) still see every entry.
+.SkillsLanding__communityItem[data-hidden="true"] {
+ display: none;
+}
+
+// These caption paragraphs are scoped under .SkillsLanding__ecosystem so
+// they outweigh the SDS global `.sds-theme-light p / .sds-theme-dark p`
+// rule (specificity 0,1,1), which otherwise forces every
to 16px /
+// 1.75rem line-height. A bare `.SkillsLanding__communityCount` (0,1,0)
+// loses to it; the ancestor prefix (0,2,0) wins.
+.SkillsLanding__ecosystem .SkillsLanding__communityEmpty {
+ font-family: "Inter", sans-serif;
+ font-size: pxToRem(12px);
+ line-height: pxToRem(18px);
+ color: var(--sds-clr-gray-11);
+ margin: pxToRem(8px) 0 0 0;
+}
+
+// "Showing x of y skills" status line under the community grid.
+// Matches the ecosystem card title (h3.SkillsCard__title): 14px Inter
+// semibold in gray-12.
+.SkillsLanding__ecosystem .SkillsLanding__communityCount {
+ font-family: "Inter", sans-serif;
+ font-size: pxToRem(14px);
+ font-weight: 600;
+ line-height: pxToRem(20px);
+ color: var(--sds-clr-gray-12);
+ margin: pxToRem(12px) 0 0 0;
+}
+
+.SkillsLanding__communityPagination {
+ display: flex;
+ justify-content: center;
+ margin-top: pxToRem(16px);
+}
+
+// "Add your skill" block at the bottom of the community panel.
+.SkillsLanding__addSkill {
+ margin-top: pxToRem(24px);
+ padding-top: pxToRem(16px);
+ border-top: 1px solid var(--sds-clr-gray-06);
+}
+
+h3.SkillsLanding__addSkillTitle {
+ font-family: "Inter", sans-serif;
+ font-size: pxToRem(14px);
+ font-weight: 600;
+ line-height: pxToRem(20px);
+ margin: 0 0 pxToRem(4px) 0;
+ color: var(--sds-clr-gray-12);
+}
+
+.SkillsLanding__ecosystem p.SkillsLanding__addSkillText {
+ font-family: "Inter", sans-serif;
+ font-size: pxToRem(12px);
+ font-weight: 400;
+ line-height: pxToRem(18px);
+ margin: 0 0 pxToRem(12px) 0;
+ color: var(--sds-clr-gray-11);
+
+ a {
+ color: var(--sds-clr-gray-12);
+ }
+
+ code {
+ font-family: "Roboto Mono", monospace;
+ }
+}
+
+.SkillsLanding__addSkillSnippet {
+ font-family: "Roboto Mono", monospace;
+ font-size: pxToRem(12px);
+ line-height: pxToRem(18px);
+ color: var(--sds-clr-gray-11);
+ background-color: var(--sds-clr-gray-03);
+ border: 1px solid var(--sds-clr-gray-06);
+ border-radius: pxToRem(8px);
+ padding: pxToRem(12px);
+ margin: 0;
+ overflow-x: auto;
+}
+
// Installer panel: independent tablist; only the active tab's panel
// shows. Stacks via flex column because exactly one panel is visible.
.SkillsLanding__installerPanel {
diff --git a/site/src/data/skills.ts b/site/src/data/skills.ts
index 80b6d6a..e45da66 100644
--- a/site/src/data/skills.ts
+++ b/site/src/data/skills.ts
@@ -71,7 +71,6 @@ export type EcosystemCardSource = {
pathLabel: string;
/** Full URL copied to clipboard when the user clicks the pill. */
copyValue: string;
- category: FilterType;
};
/**
@@ -142,7 +141,6 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
pathLabel: "OpenZeppelin/openzeppelin-skills",
copyValue:
"https://github.com/OpenZeppelin/openzeppelin-skills/blob/main/skills/setup-stellar-contracts/SKILL.md",
- category: "Ecosystem",
},
{
title: "DeFindex SDK",
@@ -151,7 +149,6 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
pathLabel: "paltalabs/defindex-sdk",
copyValue:
"https://github.com/paltalabs/defindex-sdk/blob/main/defindex-sdk-skill.md",
- category: "Ecosystem",
},
{
title: "Soroswap SDK",
@@ -159,7 +156,6 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
"Trade on Soroswap DEX from a backend, bot, or swap widget using the @soroswap/sdk TypeScript package. Covers token swaps, liquidity pool operations, price and route queries, API key handling, and signing flows for both server keypairs and browser wallets.",
pathLabel: "soroswap/sdk",
copyValue: "https://github.com/soroswap/sdk/blob/main/soroswap-sdk-skill.md",
- category: "Ecosystem",
},
{
title: "Trustless Work Escrow",
@@ -168,6 +164,5 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
pathLabel: "Trustless-Work/trustless-work-dev-skill",
copyValue:
"https://github.com/Trustless-Work/trustless-work-dev-skill/blob/main/SKILL.md",
- category: "Ecosystem",
},
] as const;