Files
skillhub/apps/web/app/[locale]/docs/api/page.tsx
airano 3f599a23fb sync: SEO canonical URLs, curation improvements, batch scripts, stats fix
- Dynamic canonical URLs and alternates for all pages (SEO fix)
- Shared seo.ts utility for canonical path generation
- Improved duplicate detection: standalone > aggregator priority, forks/created_at tie-breakers
- browseReadyFilter now includes unique aggregator skills
- Stats aligned with browseReadyFilter (was showing 16k instead of 62k+)
- Re-review mechanism: content_hash changes trigger needs-re-review
- review_status field added to skills schema
- Batch security scan and quality score scripts
- Indexer Dockerfile updated for curation deps
- Removed roadmap from README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:40:03 +03:30

571 lines
21 KiB
TypeScript

import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { ApiEndpointSection } from '@/components/ApiEndpointSection';
import type { EndpointDef } from '@/components/ApiEndpointSection';
import Link from 'next/link';
import { ArrowLeft, ArrowRight, Search, FileCode, Users, Compass, Mail } from 'lucide-react';
import { getPageAlternates } from '@/lib/seo';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return {
alternates: getPageAlternates(locale, '/docs/api'),
};
}
export default async function ApiDocsPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations('docs');
const isRTL = locale === 'fa';
const ArrowIcon = isRTL ? ArrowLeft : ArrowRight;
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
const labels = {
parameters: t('api.labels.parameters'),
requestBody: t('api.labels.requestBody'),
required: t('api.labels.required'),
optional: t('api.labels.optional'),
default: t('api.labels.default'),
responseExample: t('api.labels.responseExample'),
rateLimit: t('api.labels.rateLimit'),
cache: t('api.labels.cache'),
authRequired: t('api.labels.authRequired'),
notes: t('api.labels.notes'),
};
// --- Section 1: Skills ---
const skillsEndpoints: EndpointDef[] = [
{
method: 'GET',
path: '/api/skills',
description: t('api.endpoints.searchSkills'),
auth: false,
rateLimit: '60 req/min',
cacheTTL: '5 min',
params: [
{ name: 'q', type: 'string', required: false, description: t('api.params.q') },
{ name: 'category', type: 'string', required: false, description: t('api.params.category') },
{ name: 'platform', type: 'string', required: false, description: t('api.params.platform') + ' (claude, codex, copilot, cursor, windsurf)' },
{ name: 'format', type: 'string', required: false, description: t('api.params.format'), default: 'skill.md' },
{ name: 'verified', type: 'boolean', required: false, description: t('api.params.verified') },
{ name: 'minStars', type: 'number', required: false, description: t('api.params.minStars') },
{ name: 'sort', type: 'string', required: false, description: t('api.params.sort') + ' (stars, downloads, rating, recent)', default: 'downloads' },
{ name: 'page', type: 'number', required: false, description: t('api.params.page'), default: '1' },
{ name: 'limit', type: 'number', required: false, description: t('api.params.limit'), default: '20' },
],
responseExample: `{
"skills": [
{
"id": "anthropic/skills/code-review",
"name": "code-review",
"description": "AI-powered code review assistant",
"githubOwner": "anthropic",
"githubRepo": "skills",
"githubStars": 1234,
"downloadCount": 567,
"securityStatus": "PASS",
"rating": 4.5,
"ratingCount": 23,
"isVerified": true,
"compatibility": { "platforms": ["claude", "cursor"] }
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8
},
"searchEngine": "meilisearch"
}`,
notes: t('api.notes.searchFallback'),
},
{
method: 'GET',
path: '/api/skills/:id',
description: t('api.endpoints.getSkill'),
auth: false,
rateLimit: '120 req/min',
responseExample: `{
"id": "anthropic/skills/code-review",
"name": "code-review",
"description": "AI-powered code review assistant",
"githubOwner": "anthropic",
"githubRepo": "skills",
"skillPath": "code-review",
"branch": "main",
"version": "1.0.0",
"license": "MIT",
"githubStars": 1234,
"downloadCount": 567,
"securityScore": 95,
"securityStatus": "PASS",
"isVerified": true,
"compatibility": { "platforms": ["claude"] },
"rawContent": "# Code Review\\n...",
"sourceFormat": "skill.md"
}`,
notes: t('api.notes.viewCount'),
},
{
method: 'GET',
path: '/api/skills/featured',
description: t('api.endpoints.featuredSkills'),
auth: false,
rateLimit: '120 req/min',
cacheTTL: '2 hours',
params: [
{ name: 'limit', type: 'number', required: false, description: t('api.params.limitNum'), default: '6' },
],
},
{
method: 'GET',
path: '/api/skills/recent',
description: t('api.endpoints.recentSkills'),
auth: false,
rateLimit: '120 req/min',
cacheTTL: '1 hour',
params: [
{ name: 'limit', type: 'number', required: false, description: t('api.params.limitNum'), default: '10' },
],
},
{
method: 'POST',
path: '/api/skills/install',
description: t('api.endpoints.trackInstall'),
auth: false,
bodyParams: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
{ name: 'platform', type: 'string', required: false, description: t('api.params.platform') },
{ name: 'method', type: 'string', required: false, description: t('api.params.method') },
],
responseExample: `{ "success": true, "skillId": "owner/repo/skill", "platform": "claude", "method": "cli" }`,
notes: t('api.notes.installDedup'),
},
{
method: 'POST',
path: '/api/skills/add-request',
description: t('api.endpoints.addRequest'),
auth: true,
bodyParams: [
{ name: 'repositoryUrl', type: 'string', required: true, description: t('api.params.repositoryUrl') },
{ name: 'reason', type: 'string', required: false, description: t('api.params.reason') },
],
responseExample: `{ "success": true, "requestId": "uuid", "hasSkillMd": true, "skillCount": 3 }`,
},
{
method: 'POST',
path: '/api/skills/removal-request',
description: t('api.endpoints.removalRequest'),
auth: true,
bodyParams: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
{ name: 'reason', type: 'string', required: false, description: t('api.params.reason') },
],
responseExample: `{ "success": true, "requestId": "uuid", "blocked": true }`,
},
];
// --- Section 2: Skill Files ---
const skillFilesEndpoints: EndpointDef[] = [
{
method: 'GET',
path: '/api/skill-files',
description: t('api.endpoints.getSkillFiles'),
auth: false,
rateLimit: '60 req/min',
params: [
{ name: 'id', type: 'string', required: true, description: t('api.params.skillId') },
],
responseExample: `{
"skillId": "owner/repo/skill",
"githubOwner": "owner",
"githubRepo": "repo",
"skillPath": "skill",
"branch": "main",
"files": [
{
"name": "SKILL.md",
"path": "SKILL.md",
"type": "file",
"size": 2048,
"content": "# Skill Name\\n..."
}
],
"fromCache": true,
"cachedAt": "2026-01-15T10:30:00Z"
}`,
notes: t('api.notes.cacheInDB'),
},
{
method: 'GET',
path: '/api/skill-files/zip',
description: t('api.endpoints.downloadZip'),
auth: false,
rateLimit: '60 req/min',
params: [
{ name: 'id', type: 'string', required: true, description: t('api.params.skillId') },
{ name: 'platform', type: 'string', required: false, description: t('api.params.platformZip'), default: 'claude' },
],
notes: t('api.notes.zipTransform'),
},
];
// --- Section 3: User Actions ---
const userActionsEndpoints: EndpointDef[] = [
{
method: 'GET',
path: '/api/ratings',
description: t('api.endpoints.getRatings'),
auth: false,
rateLimit: '120 req/min',
params: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
{ name: 'limit', type: 'number', required: false, description: t('api.params.limitNum'), default: '10' },
{ name: 'offset', type: 'number', required: false, description: t('api.params.offset'), default: '0' },
],
responseExample: `{
"ratings": [
{
"id": "uuid",
"rating": 5,
"review": "Great skill!",
"createdAt": "2026-01-10T00:00:00Z",
"user": { "id": "uid", "username": "user1", "avatarUrl": "https://..." }
}
],
"summary": { "average": 4.5, "count": 23 }
}`,
},
{
method: 'POST',
path: '/api/ratings',
description: t('api.endpoints.createRating'),
auth: true,
rateLimit: '600 req/min',
bodyParams: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
{ name: 'rating', type: 'number', required: true, description: t('api.params.rating') },
{ name: 'review', type: 'string', required: false, description: t('api.params.review') },
],
responseExample: `{ "rating": { "id": "uuid", "rating": 5, "review": "..." }, "summary": { "average": 4.5, "count": 24 } }`,
},
{
method: 'GET',
path: '/api/ratings/me',
description: t('api.endpoints.getMyRating'),
auth: true,
rateLimit: '600 req/min',
params: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
],
responseExample: `{ "rating": { "id": "uuid", "rating": 5, "review": "Great!" } }`,
},
{
method: 'GET',
path: '/api/favorites',
description: t('api.endpoints.getFavorites'),
auth: true,
rateLimit: '600 req/min',
responseExample: `{
"favorites": [
{
"id": "owner/repo/skill",
"name": "skill-name",
"description": "...",
"githubStars": 100,
"downloadCount": 50,
"securityStatus": "PASS",
"isVerified": true,
"rating": 4.5,
"ratingCount": 10
}
]
}`,
},
{
method: 'POST',
path: '/api/favorites',
description: t('api.endpoints.addFavorite'),
auth: true,
rateLimit: '600 req/min',
bodyParams: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
],
responseExample: `{ "success": true, "favorited": true }`,
},
{
method: 'DELETE',
path: '/api/favorites',
description: t('api.endpoints.removeFavorite'),
auth: true,
rateLimit: '600 req/min',
bodyParams: [
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
],
responseExample: `{ "success": true, "favorited": false }`,
},
{
method: 'POST',
path: '/api/favorites/check',
description: t('api.endpoints.checkFavorites'),
auth: true,
rateLimit: '600 req/min',
bodyParams: [
{ name: 'skillIds', type: 'string[]', required: true, description: t('api.params.skillIds') },
],
responseExample: `{ "favorited": { "owner/repo/skill-a": true, "owner/repo/skill-b": false } }`,
},
];
// --- Section 4: Discovery & Metadata ---
const discoveryEndpoints: EndpointDef[] = [
{
method: 'GET',
path: '/api/categories',
description: t('api.endpoints.getCategories'),
auth: false,
rateLimit: '120 req/min',
cacheTTL: '12 hours',
responseExample: `{
"categories": [
{
"id": "cat-id",
"name": "Code Quality",
"slug": "code-quality",
"description": "...",
"icon": "🔍",
"skillCount": 15,
"sortOrder": 1
}
]
}`,
},
{
method: 'GET',
path: '/api/stats',
description: t('api.endpoints.getStats'),
auth: false,
rateLimit: '120 req/min',
cacheTTL: '1 hour',
responseExample: `{
"totalSkills": 850,
"totalDownloads": 12500,
"totalCategories": 23,
"totalContributors": 320,
"platforms": 5
}`,
},
{
method: 'GET',
path: '/api/health',
description: t('api.endpoints.healthCheck'),
auth: false,
responseExample: `{
"status": "healthy",
"timestamp": "2026-02-11T12:00:00Z",
"version": "0.2.4",
"isPrimary": true,
"services": {
"database": { "status": "healthy", "latency": 5 },
"meilisearch": { "status": "healthy", "latency": 12 },
"redis": { "status": "healthy", "latency": 3 }
}
}`,
},
{
method: 'GET',
path: '/api/attribution',
description: t('api.endpoints.getAttribution'),
auth: false,
rateLimit: '120 req/min',
cacheTTL: '1 hour',
responseExample: `{
"totalSkills": 850,
"totalContributors": 320,
"totalRepos": 150,
"licenseDistribution": [
{ "license": "MIT", "count": 50, "percentage": 50 }
],
"lastUpdated": "2026-02-11T00:00:00Z"
}`,
},
];
// --- Section 5: Newsletter ---
const newsletterEndpoints: EndpointDef[] = [
{
method: 'POST',
path: '/api/newsletter/subscribe',
description: t('api.endpoints.subscribe'),
auth: false,
rateLimit: '60 req/min',
bodyParams: [
{ name: 'email', type: 'string', required: true, description: t('api.params.email') },
{ name: 'source', type: 'string', required: false, description: t('api.params.source') },
{ name: 'locale', type: 'string', required: false, description: t('api.params.locale'), default: 'en' },
],
responseExample: `{ "success": true, "subscribed": true }`,
},
{
method: 'POST',
path: '/api/newsletter/unsubscribe',
description: t('api.endpoints.unsubscribe'),
auth: false,
rateLimit: '60 req/min',
bodyParams: [
{ name: 'email', type: 'string', required: true, description: t('api.params.email') },
],
responseExample: `{ "success": true, "unsubscribed": true }`,
notes: t('api.notes.alwaysSuccess'),
},
];
const sections = [
{ title: t('api.sections.skills.title'), description: t('api.sections.skills.description'), icon: Search, endpoints: skillsEndpoints },
{ title: t('api.sections.skillFiles.title'), description: t('api.sections.skillFiles.description'), icon: FileCode, endpoints: skillFilesEndpoints },
{ title: t('api.sections.userActions.title'), description: t('api.sections.userActions.description'), icon: Users, endpoints: userActionsEndpoints },
{ title: t('api.sections.discovery.title'), description: t('api.sections.discovery.description'), icon: Compass, endpoints: discoveryEndpoints },
{ title: t('api.sections.newsletter.title'), description: t('api.sections.newsletter.description'), icon: Mail, endpoints: newsletterEndpoints },
];
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">
<section className="section bg-surface">
<div className="container-main">
<div className="max-w-4xl mx-auto">
<Link
href={`/${locale}/docs`}
className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 mb-8"
>
<ArrowIcon className="w-4 h-4 rotate-180" />
{t('title')}
</Link>
<h1 className="text-4xl font-bold mb-4">{t('api.title')}</h1>
<p className="text-lg text-text-secondary mb-10">{t('api.description')}</p>
{/* Base URL */}
<div className="mb-10">
<h2 className="text-2xl font-bold mb-3">{t('api.baseUrl')}</h2>
<div className="glass-card p-4" dir="ltr">
<code className="text-sm font-mono text-left block">{siteUrl}</code>
</div>
</div>
{/* Authentication */}
<div className="mb-10">
<h2 className="text-2xl font-bold mb-3">{t('api.authSection.title')}</h2>
<p className="text-text-secondary mb-2">{t('api.authSection.description')}</p>
<p className="text-text-secondary text-sm">{t('api.authSection.howTo')}</p>
</div>
{/* Rate Limiting */}
<div className="mb-10">
<h2 className="text-2xl font-bold mb-3">{t('api.rateLimitSection.title')}</h2>
<p className="text-text-secondary mb-4">{t('api.rateLimitSection.description')}</p>
<div className="glass-card overflow-hidden mb-4" dir="ltr">
<table className="w-full text-sm text-left">
<thead>
<tr className="border-b border-border/50 bg-surface-hover/50">
<th className="py-2.5 px-4 font-medium">{t('api.rateLimitSection.tier')}</th>
<th className="py-2.5 px-4 font-medium">{t('api.rateLimitSection.limit')}</th>
<th className="py-2.5 px-4 font-medium">{t('api.rateLimitSection.use')}</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-border/30">
<td className="py-2 px-4 font-mono text-xs">{t('api.rateLimitSection.anonymous')}</td>
<td className="py-2 px-4">120 req/min</td>
<td className="py-2 px-4 text-text-secondary">{t('api.rateLimitSection.anonymousUse')}</td>
</tr>
<tr className="border-b border-border/30">
<td className="py-2 px-4 font-mono text-xs">{t('api.rateLimitSection.search')}</td>
<td className="py-2 px-4">60 req/min</td>
<td className="py-2 px-4 text-text-secondary">{t('api.rateLimitSection.searchUse')}</td>
</tr>
<tr>
<td className="py-2 px-4 font-mono text-xs">{t('api.rateLimitSection.authenticated')}</td>
<td className="py-2 px-4">600 req/min</td>
<td className="py-2 px-4 text-text-secondary">{t('api.rateLimitSection.authenticatedUse')}</td>
</tr>
</tbody>
</table>
</div>
<div dir="ltr">
<h3 className="text-sm font-semibold mb-2 text-left">{t('api.rateLimitSection.headersTitle')}</h3>
<div className="space-y-1 text-sm text-left">
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">X-RateLimit-Limit</code> {t('api.rateLimitSection.headerLimit')}</p>
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">X-RateLimit-Remaining</code> {t('api.rateLimitSection.headerRemaining')}</p>
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">X-RateLimit-Reset</code> {t('api.rateLimitSection.headerReset')}</p>
</div>
</div>
</div>
{/* Error Responses */}
<div className="mb-10">
<h2 className="text-2xl font-bold mb-3">{t('api.errorSection.title')}</h2>
<p className="text-text-secondary mb-4">{t('api.errorSection.description')}</p>
<div className="glass-card p-4 mb-4" dir="ltr">
<pre className="text-sm font-mono overflow-x-auto text-left">{`{
"error": "Too Many Requests",
"message": "Rate limit exceeded",
"retryAfter": 45,
"limit": 60
}`}</pre>
</div>
<div dir="ltr">
<h3 className="text-sm font-semibold mb-2 text-left">{t('api.errorSection.commonCodes')}</h3>
<div className="space-y-1 text-sm text-left">
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">400</code> {t('api.errorSection.code400')}</p>
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">401</code> {t('api.errorSection.code401')}</p>
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">404</code> {t('api.errorSection.code404')}</p>
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">429</code> {t('api.errorSection.code429')}</p>
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">500</code> {t('api.errorSection.code500')}</p>
</div>
</div>
</div>
{/* Quick example */}
<div className="mb-10">
<h2 className="text-2xl font-bold mb-3">Quick Example</h2>
<div className="glass-card p-4" dir="ltr">
<code className="text-sm font-mono whitespace-pre-wrap text-left block">{`curl "${siteUrl}/api/skills?q=code-review&limit=5"`}</code>
</div>
</div>
<hr className="border-border/50 mb-10" />
{/* Endpoint Sections */}
{sections.map((section, index) => (
<ApiEndpointSection
key={index}
title={section.title}
description={section.description}
endpoints={section.endpoints}
labels={labels}
/>
))}
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}