Add stellar skills site#18
Conversation
A Next.js 15 static-export site at site/ that mirrors every SKILL.md under skills/ and exposes them as a copy-pastable directory at the deploy origin, plus a /llms.txt index for AI agents. GitHub Pages deploy is branch-based via two workflows at .github/: - deploy-pages.yml publishes main to gh-pages root on push to main. - preview-pr.yml publishes PR previews to gh-pages:/pr/<N>/ with a bot comment containing the URL; cleans up on PR close. Both workflows pin SITE_ORIGIN to production so the hero pill, copy-pastable card URLs, and llms.txt show post-merge URLs even on preview builds. An IS_PREVIEW banner identifies preview builds. Custom-domain cutover is a one-place repo-variable change. Co-Authored-By: mk <86380734+minkyeongshin@users.noreply.github.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Introduces a new Next.js 15 static site under site/ that mirrors the repo's skills/*/SKILL.md files, exposes them as copy-pastable URLs at skills.stellar.org, and emits a sibling /llms.txt index for AI agents. Includes GitHub Pages workflows for main deploys and per-PR previews on the gh-pages branch.
Changes:
- Add full Next.js (App Router, static export) site with server-rendered landing page, theme switch, copy-to-clipboard pills, ARIA tablist filtering for skills and installer methods, and 404/error boundaries.
- Add build scripts that mirror
../skills/intopublic/skills/and generatepublic/llms.txtfromsrc/data/skills.tsplus upstream frontmatter. - Add two GitHub Actions workflows (
deploy-pages.yml,preview-pr.yml) that publish togh-pages(root formain,pr/<N>/for previews) and pinSITE_ORIGINto production.
Reviewed changes
Copilot reviewed 29 out of 35 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| site/src/app/page.tsx | Landing page composition: hero, install tabs, skill grid, ecosystem grid, footer. |
| site/src/app/layout.tsx | Root layout, fonts, GA, theme class. |
| site/src/app/error.tsx, not-found.tsx, global-error.tsx, icon.svg | Error/404 pages and favicon. |
| site/src/app/_components/{SkillCard,SkillsFilter,CopyButton,ThemeSwitchIsland,icons}.tsx | Server card + client islands and inline SVG icons. |
| site/src/data/skills.ts, installers.mjs | Source-of-truth for cards, filters, installers. |
| site/src/lib/skill-meta.mjs | Frontmatter + first-H1 parser shared between page and llms.txt. |
| site/src/styles/{globals,utils}.scss, app/styles.scss | Global styles, px→rem helpers, landing-page styles, CSS-driven filtering. |
| site/scripts/copy-skills.mjs | Mirrors upstream SKILL.md files into public/. |
| site/scripts/generate-llms-txt.mjs | Regex-parses skills.ts to emit public/llms.txt. |
| site/{package.json,tsconfig.json,next.config.js,.eslintrc.json,.prettierrc.json,.gitignore,.env.example,README.md,CLAUDE.md} | Project configuration and docs. |
| .github/workflows/{deploy-pages,preview-pr}.yml | Main + per-PR preview deployment to GitHub Pages. |
Comments suppressed due to low confidence (1)
site/src/app/_components/CopyButton.tsx:37
- The
setTimeoutcallback can fire after the component unmounts (e.g., user changes the active filter tab so the button is removed before 1s elapses), causing a state update on an unmounted component. Track the timeout id and clear it in auseEffectcleanup, or guard the callback with a mounted ref.
setCopied(true);
setTimeout(() => setCopied(false), 1000);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await navigator.clipboard.writeText(value); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1000); |
| const parseArray = (arrayName) => { | ||
| const re = new RegExp( | ||
| `${arrayName}[^=]*=\\s*\\[([\\s\\S]*?)\\]\\s*as\\s+const`, | ||
| ); | ||
| const m = re.exec(source); | ||
| if (!m) return []; | ||
| const body = m[1]; | ||
| const entries = []; | ||
| const objectRe = /\{([\s\S]*?)\},/g; | ||
| let om; | ||
| while ((om = objectRe.exec(body)) !== null) { | ||
| const fields = om[1]; | ||
| const get = (key) => { | ||
| const fr = new RegExp(`\\b${key}:\\s*"([^"]+)"`); | ||
| const fm = fr.exec(fields); | ||
| return fm ? fm[1] : null; | ||
| }; | ||
| entries.push({ | ||
| source: get("source"), | ||
| title: get("title"), | ||
| description: get("description"), | ||
| copyValue: get("copyValue"), | ||
| category: get("category"), | ||
| }); | ||
| } | ||
| return entries; | ||
| }; | ||
|
|
||
| const parseFilters = () => { | ||
| const re = /FILTERS:\s*readonly[^=]*=\s*\[([\s\S]*?)\]\s*as\s+const/; | ||
| const m = re.exec(source); | ||
| if (!m) return []; | ||
| return [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]); | ||
| }; |
| // Clean stale files before copying so removed skills don't linger. | ||
| rmSync(PUBLIC_SKILLS_DIR, { recursive: true, force: true }); | ||
|
|
||
| const missing = []; | ||
| for (const source of sources) { | ||
| const src = join(REPO_ROOT, source); | ||
| const dest = join(PUBLIC_DIR, source); | ||
| if (!existsSync(src)) { | ||
| missing.push(source); | ||
| continue; | ||
| } | ||
| mkdirSync(dirname(dest), { recursive: true }); | ||
| cpSync(src, dest, { dereference: false }); | ||
| } | ||
|
|
||
| // Apache-2.0 attribution alongside the content. | ||
| const upstreamLicense = join(REPO_ROOT, "LICENSE"); | ||
| if (existsSync(upstreamLicense)) { | ||
| mkdirSync(PUBLIC_SKILLS_DIR, { recursive: true }); | ||
| cpSync(upstreamLicense, join(PUBLIC_SKILLS_DIR, "LICENSE"), { | ||
| dereference: false, | ||
| }); | ||
| } | ||
|
|
||
| if (missing.length > 0) { | ||
| const lines = missing.map((s) => ` ${s}`).join("\n"); | ||
| const msg = `[copy-skills] ${missing.length} advertised source(s) missing under ${UPSTREAM_SKILLS_DIR}:\n${lines}`; | ||
| if (strict) { | ||
| console.error(msg); | ||
| process.exit(1); | ||
| } | ||
| console.warn(msg); | ||
| } |
| return ( | ||
| <html lang="en"> | ||
| <body className="sds-theme-light" data-sds-theme="sds-theme-light"> | ||
| <div id="root">{children}</div> | ||
| {GA_TRACKING_ENABLED && <GoogleTagManager gtmId="GTM-KCNDDL3" />} | ||
| </body> | ||
| </html> |
| const parseFrontmatter = (content) => { | ||
| const match = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(content); | ||
| if (!match) return { frontmatter: {}, body: content }; | ||
| const frontmatter = {}; | ||
| for (const line of match[1].split("\n")) { | ||
| const kv = /^([\w-]+):\s*(.*)$/.exec(line); | ||
| if (!kv) continue; | ||
| let value = kv[2].trim(); | ||
| if ( | ||
| (value.startsWith('"') && value.endsWith('"')) || | ||
| (value.startsWith("'") && value.endsWith("'")) | ||
| ) { | ||
| value = value.slice(1, -1); | ||
| } | ||
| frontmatter[kv[1]] = value; | ||
| } | ||
| return { frontmatter, body: content.slice(match[0].length) }; | ||
| }; |
| export default function GlobalError() { | ||
| return ( | ||
| <html> | ||
| <body> | ||
| <NextError statusCode={0} /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } |
| contents: write | ||
|
|
||
| concurrency: | ||
| group: pages-main |
| # displayed text. Asset paths (in <script src=…/_next/…>) are | ||
| # excluded by requiring the match to live inside a text node. | ||
| if grep -qE "Read [^<]*pr/[0-9]" out/index.html; then |
| // Fonts for SDS | ||
| // TODO: switch to next/font/google to avoid render-blocking external CSS | ||
| @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Roboto+Mono&display=swap"); |
| > .Button { | ||
| padding-left: 0; | ||
| padding-right: 0; | ||
| border-color: transparent !important; | ||
| background-color: transparent !important; | ||
| margin-bottom: pxToRem(12px); | ||
|
|
||
| @media (hover: hover) { | ||
| &:hover { | ||
| background-color: transparent !important; | ||
| color: var(--sds-clr-lilac-11) !important; | ||
| } | ||
| } | ||
|
|
||
| &:focus, | ||
| &:focus-visible { | ||
| outline: none !important; | ||
| box-shadow: none !important; | ||
| } | ||
| } |
…trict TS, build scripts
No description provided.