docs(skills): agent skills (#42148)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Docs update — Rewrite the Agent Skills documentation as a single flat
page with a skills table.

## What is the current behavior?

The Agent Skills docs have a nested structure with an index page and
individual dynamic pages for each skill (fetched from the
`supabase/agent-skills` repo). Skills also inject sub-items into the
sidebar navigation.

## What is the new behavior?

This PR replaces the nested skill pages with a single, flat Agent Skills
page that:

- Lists all skills in a **table** with name, description, and a
**copy-to-install button** (fetched dynamically from the
[supabase/agent-skills](https://github.com/supabase/agent-skills) repo)
- Includes **installation commands** for both the skills CLI (`npx
skills add`) and Claude Code plugins
- Links skill names directly to their source on GitHub instead of
rendering full skill content inline
- Removes the dynamic `[slug]` route, sidebar nav injection, and local
skill example files

### Navigation Structure

```
Start
  > AI Tools
      > Agent Skills   (new)
      > Prompts        (existing)
      > Supabase MCP server (existing)
```

Closes
[AI-361](https://linear.app/supabase/issue/AI-361/create-skills-documentation-page-in-ai-tooling-docs)

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-03-24 10:18:54 +00:00
1 parent e09a663292
commit 6153497500
9 files changed
+262 -1

No files matched your search

@@ -0,0 +1,103 @@
import matter from 'gray-matter'
import { cache } from 'react'
const SKILLS_REPO = {
org: 'supabase',
repo: 'agent-skills',
branch: 'main',
path: 'skills',
}
interface SkillMetadata {
name?: string
title?: string
description?: string
}
interface SkillSummary {
name: string
description: string
installCommand: string
}
interface GitHubContentItem {
name: string
path: string
type: 'file' | 'dir'
}
async function fetchGitHubDirectory(path: string): Promise<GitHubContentItem[]> {
const url = `https://api.github.com/repos/${SKILLS_REPO.org}/${SKILLS_REPO.repo}/contents/${path}?ref=${SKILLS_REPO.branch}`
let response: Response
try {
response = await fetch(url, {
headers: {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'Supabase-Docs',
},
next: { revalidate: 3600 },
})
} catch (err) {
throw new Error(`Failed to fetch agent skills directory from GitHub (network error)`, {
cause: err,
})
}
if (!response.ok) {
throw new Error(
`Failed to fetch agent skills directory from GitHub: ${response.status} ${response.statusText}`
)
}
const data = await response.json()
if (!Array.isArray(data)) {
throw new Error('Expected directory listing from GitHub agent skills repo')
}
return data
}
async function fetchGitHubFile(path: string): Promise<string> {
const url = `https://raw.githubusercontent.com/${SKILLS_REPO.org}/${SKILLS_REPO.repo}/${SKILLS_REPO.branch}/${path}`
let response: Response
try {
response = await fetch(url, {
next: { revalidate: 3600 },
})
} catch (err) {
throw new Error(`Failed to fetch agent skill file from GitHub (network error)`, { cause: err })
}
if (!response.ok) {
throw new Error(
`Failed to fetch agent skill file from GitHub: ${response.status} ${response.statusText}`
)
}
return await response.text()
}
async function getAiSkillsImpl(): Promise<SkillSummary[]> {
const directories = await fetchGitHubDirectory(SKILLS_REPO.path)
const skillDirs = directories.filter((item) => item.type === 'dir')
const skills = await Promise.all(
skillDirs.map(async (item) => {
const skillPath = `${SKILLS_REPO.path}/${item.name}/SKILL.md`
const rawContent = await fetchGitHubFile(skillPath)
const { data } = matter(rawContent) as { data: SkillMetadata }
return {
name: item.name,
description: data.description || '',
installCommand: `npx skills add supabase/agent-skills --skill ${item.name}`,
}
})
)
return skills.sort((a, b) => a.name.localeCompare(b.name))
}
export const getAiSkills = cache(getAiSkillsImpl)
@@ -0,0 +1,60 @@
import { getAiSkills } from './AiSkills.utils'
import { CopyButton } from './CopyButton'
export async function AiSkillsIndex() {
let skills: Awaited<ReturnType<typeof getAiSkills>> = []
try {
skills = await getAiSkills()
} catch {
// Swallow errors from getAiSkills to keep the page usable
}
if (!skills.length) {
return (
<div className="not-prose text-sm text-foreground-lighter">
Unable to load AI skills at the moment.
</div>
)
}
return (
<div className="not-prose overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-default">
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Skill</th>
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Description</th>
<th className="text-left py-2 text-foreground-lighter font-medium">Install command</th>
</tr>
</thead>
<tbody>
{skills.map((skill) => (
<tr key={skill.name} className="border-b border-default">
<td className="py-3 pr-4 font-mono text-xs whitespace-nowrap">
<a
href={`https://github.com/supabase/agent-skills/tree/main/skills/${skill.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-brand transition-colors"
>
{skill.name}
</a>
</td>
<td className="py-3 pr-4 text-foreground-lighter">{skill.description}</td>
<td className="w-px p-0">
<div className="h-full max-w-xs overflow-x-auto flex items-center py-3">
<div className="flex items-center gap-2 whitespace-nowrap">
<CopyButton text={skill.installCommand} />
<code className="font-mono text-xs text-foreground-lighter">
{skill.installCommand}
</code>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
@@ -0,0 +1,29 @@
'use client'
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
import { cn } from 'ui'
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
type="button"
onClick={handleCopy}
className={cn(
'inline-flex items-center rounded-md border p-1.5 transition-colors',
'border-default bg-surface-100 text-foreground-lighter hover:bg-surface-200 hover:text-foreground'
)}
title={copied ? 'Copied!' : 'Copy to clipboard'}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
)
}
@@ -3,7 +3,9 @@ import Layout from '~/layouts/guides'
import { getAiPrompts } from '../getting-started/ai-prompts/[slug]/AiPrompts.utils'
export default async function GettingStartedLayout({ children }: { children: React.ReactNode }) {
const additionalNavItems = { prompts: await getPrompts() }
const additionalNavItems = {
prompts: await getPrompts(),
}
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
+12
View File
@@ -176,6 +176,18 @@ function useBreadcrumbs() {
return breadcrumbs
}
// TODO: Breadcrumbs currently can't infer the "AI Tools" parent for /guides/getting-started/ai-* routes,
// so we special-case these paths here. Remove when Breadcrumbs can derive this hierarchy from NavigationMenu.
const isAiSkillsPage = pathname.startsWith('/guides/getting-started/ai-skills')
if (isAiSkillsPage) {
const breadcrumbs = [
{ name: 'Getting started', url: '/guides/getting-started' },
{ name: 'AI Tools' },
{ name: 'Agent Skills', url: '/guides/getting-started/ai-skills' },
]
return breadcrumbs
}
const menuId = getMenuId(pathname)
const menu = NavItems[menuId]
return findMenuItemByUrl(menu, pathname, [])
@@ -499,6 +499,10 @@ export const gettingstarted: NavMenuConstant = {
name: 'Prompts',
url: '/guides/getting-started/ai-prompts' as `/${string}`,
},
{
name: 'Agent Skills',
url: '/guides/getting-started/ai-skills' as `/${string}`,
},
{
name: 'Supabase MCP server',
url: '/guides/getting-started/mcp' as `/${string}`,
@@ -0,0 +1,48 @@
---
title: Agent Skills
---
Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. Agents are increasingly capable, but often don't have the context they need to do real work reliably. Skills solve this by giving agents access to procedural knowledge and company-, team-, and user-specific context they can load on demand. Agents with access to a set of skills can extend their capabilities based on the task they're working on.
## Installing skills
Install all Supabase skills using the skills CLI:
```bash
npx skills add supabase/agent-skills
```
To install a specific skill from the repository:
```bash
npx skills add supabase/agent-skills --skill SKILL_NAME
```
### Claude Code plugin
You can also install the skills as Claude Code plugins:
```bash
/plugin marketplace add supabase/agent-skills
/plugin install postgres-best-practices@supabase-agent-skills
```
Skills work with 18+ AI agents including Claude Code, GitHub Copilot, Cursor, Cline, and many others.
## Available skills
<AiSkillsIndex />
## Finding more skills
Browse the [skills.sh directory](https://skills.sh) to discover skills from the community. You can also search for skills using the CLI:
```bash
npx skills find QUERY
```
## Learn more
- [Agent Skills Repository](https://github.com/supabase/agent-skills)
- [Agent Skills Documentation](https://agentskills.io/home)
- [Agent Skills Overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
@@ -7,6 +7,7 @@ import { IconPanel } from 'ui-patterns/IconPanel'
import SqlToRest from 'ui-patterns/SqlToRest'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import { AiPromptsIndex } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex'
import { AiSkillsIndex } from '~/app/guides/getting-started/ai-skills/AiSkillsIndex'
import { AppleSecretGenerator } from '~/components/AppleSecretGenerator'
import AuthProviders from '~/components/AuthProviders'
import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig'
@@ -44,6 +45,7 @@ const components = {
AccordionItem,
Admonition: AdmonitionWithMargin,
AiPromptsIndex,
AiSkillsIndex,
AuthSmsProviderConfig,
AppleSecretGenerator,
AuthProviders,
+1
View File
@@ -37,6 +37,7 @@ may_uppercase = [
"Clerk",
"Cloudflare",
"Cloudflare Workers?",
"Claude Code",
"Code Exchange",
"Colab",
"Compute",