import { getTranslations, setRequestLocale } from 'next-intl/server'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { headers } from 'next/headers'; import { Star, Download, Shield, CheckCircle, Copy, ExternalLink, Github, Calendar, User, Tag, ChevronRight, Eye } from 'lucide-react'; import { Header } from '@/components/Header'; import { Footer } from '@/components/Footer'; import { FavoriteButton } from '@/components/FavoriteButton'; import { RatingStars } from '@/components/RatingStars'; import { InstallSection } from '@/components/InstallSection'; import { ShareButton } from '@/components/ShareButton'; import { createDb, skillQueries } from '@skillhub/db'; import { FORMAT_LABELS } from 'skillhub-core'; import { formatCompactNumber } from '@/lib/format-number'; import { shouldCountView, getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache'; import type { Metadata } from 'next'; import { getPageAlternates } from '@/lib/seo'; // Force dynamic rendering to fetch fresh data from database export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { params: Promise<{ locale: string; id: string[] }>; }): Promise { const { locale, id } = await params; const skillId = id.join('/'); // Optionally fetch skill name for title const dbSkill = await getSkill(skillId); const title = dbSkill ? `${dbSkill.name} | SkillHub` : 'SkillHub'; const description = dbSkill ? dbSkill.description : undefined; return { title, description, alternates: getPageAlternates(locale, `/skill/${skillId}`), }; } interface SkillPageProps { params: Promise<{ locale: string; id: string[] }>; } // Get skill with Redis caching (1 hour TTL) async function getSkill(skillId: string) { try { return await getOrSetCache(cacheKeys.skillDetail(skillId), cacheTTL.skill, async () => { const db = createDb(); return await skillQueries.getById(db, skillId); }); } catch (error) { console.error('Error fetching skill:', error); return null; } } export default async function SkillPage({ params }: SkillPageProps) { const { locale, id } = await params; setRequestLocale(locale); const t = await getTranslations('skill'); const tCommon = await getTranslations('common'); const skillId = id.join('/'); const isRTL = locale === 'fa'; // Get skill from database const dbSkill = await getSkill(skillId); if (!dbSkill) { notFound(); } // Check if skill is blocked (removed by owner request) if (dbSkill.isBlocked) { notFound(); } // Track view count with IP-based rate limiting (1 hour cooldown per IP) // Get client IP from headers (works with Cloudflare, nginx, etc.) const headersList = await headers(); const clientIp = headersList.get('cf-connecting-ip') || headersList.get('x-real-ip') || headersList.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'; // Only increment on primary server (mirror DB is read-only) const isPrimary = process.env.IS_PRIMARY_SERVER !== 'false'; if (isPrimary) { const db = createDb(); shouldCountView(dbSkill.id, clientIp).then((shouldCount) => { if (shouldCount) { skillQueries.incrementViews(db, dbSkill.id).catch(() => { }); } }).catch(() => { }); } // Map database response to expected format const skill = { id: dbSkill.id, name: dbSkill.name, description: dbSkill.description, longDescription: dbSkill.rawContent || dbSkill.description, version: dbSkill.version || null, license: dbSkill.license || 'MIT', author: dbSkill.githubOwner, repo: dbSkill.githubRepo, repository: `https://github.com/${dbSkill.githubOwner}/${dbSkill.githubRepo}`, homepage: dbSkill.homepage || null, stars: dbSkill.githubStars || 0, downloads: dbSkill.downloadCount || 0, views: dbSkill.viewCount || 0, securityStatus: dbSkill.securityStatus || 'pass', isVerified: dbSkill.isVerified || false, createdAt: dbSkill.createdAt, updatedAt: dbSkill.updatedAt ? new Date(dbSkill.updatedAt).toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US') : 'N/A', rating: dbSkill.rating || 0, ratingCount: dbSkill.ratingCount || 0, sourceFormat: dbSkill.sourceFormat || 'skill.md', }; // Content section title based on source format (uses FORMAT_LABELS from skillhub-core) const getContentTitle = (format: string) => { const label = FORMAT_LABELS[format as keyof typeof FORMAT_LABELS] || FORMAT_LABELS['skill.md']; if (format === 'copilot-instructions') { return isRTL ? 'دستورالعمل Copilot' : label; } return isRTL ? `محتوای ${label}` : `${label} Content`; }; // Source format badge configuration (for non-SKILL.md formats) const FORMAT_PLATFORMS: Record = { 'agents.md': 'Codex', 'cursorrules': 'Cursor', 'windsurfrules': 'Windsurf', 'copilot-instructions': 'Copilot', }; const getSourceFormatBadge = (format: string) => { const platform = FORMAT_PLATFORMS[format]; if (!platform) return null; const label = FORMAT_LABELS[format as keyof typeof FORMAT_LABELS] || format; return { label, platform }; }; const sourceFormatBadge = getSourceFormatBadge(skill.sourceFormat); const getSecurityConfig = (status: string) => { switch (status) { case 'pass': return { label: t('security.pass'), icon: '✓', bg: 'bg-success/10', text: 'text-success', border: 'border-success/20' }; case 'warning': return { label: t('security.warning'), icon: '⚠', bg: 'bg-warning/10', text: 'text-warning', border: 'border-warning/20' }; case 'fail': return { label: t('security.fail'), icon: '✕', bg: 'bg-error/10', text: 'text-error', border: 'border-error/20' }; default: return { label: t('security.pass'), icon: '✓', bg: 'bg-success/10', text: 'text-success', border: 'border-success/20' }; } }; const securityConfig = getSecurityConfig(skill.securityStatus); const installCommands = { claude: { cli: `npx skillhub install ${skillId}`, path: `~/.claude/skills/${skill.name}/`, }, codex: { cli: `npx skillhub install ${skillId} --platform codex`, path: `~/.codex/skills/${skill.name}/`, }, copilot: { cli: `npx skillhub install ${skillId} --platform copilot`, path: `.github/instructions/${skill.name}.instructions.md`, }, cursor: { cli: `npx skillhub install ${skillId} --platform cursor`, path: `.cursor/rules/${skill.name}.mdc`, }, windsurf: { cli: `npx skillhub install ${skillId} --platform windsurf`, path: `.windsurf/rules/${skill.name}.md`, }, }; return (
{/* Hero Section */}
{/* Breadcrumb */} {/* Main Header */}
{/* Left: Title & Description */}

{skill.name}

{skill.isVerified && ( {tCommon('verified')} )} {securityConfig.label} {sourceFormatBadge && ( {sourceFormatBadge.platform} )}

{skill.description}

{/* Author, Version, License & Last Update */}
@{skill.author}
{skill.version && ( <> v{skill.version} )} {skill.license} {skill.updatedAt}
{/* Right: Actions */}
{/* Project Configuration Warning Banner (non-SKILL.md) */} {sourceFormatBadge && (

{isRTL ? `این یک فایل پیکربندی اختصاصی پروژه (${sourceFormatBadge.label}) است، نه یک مهارت عامل قابل استفاده مجدد. حاوی دستورالعمل‌هایی است که برای مخزن ${skill.repo} طراحی شده و ممکن است در پروژه‌های دیگر کاربردی نباشد.` : `This is a project-specific configuration file (${sourceFormatBadge.label}), not a reusable Agent Skill. It contains instructions designed for the ${skill.repo} repository and may not be applicable to other projects.`}

{isRTL ? 'مرور مهارت‌های قابل استفاده مجدد' : 'Browse reusable skills'}
)} {/* Stale Skill Warning Banner */} {dbSkill.isStale && (

{isRTL ? 'این مهارت ممکن است از مخزن GitHub اصلی حذف یا جابجا شده باشد. فایل‌ها از حافظه پنهان SkillHub ارائه می‌شوند و ممکن است قدیمی باشند.' : 'This skill may have been removed or moved from its GitHub repository. Files are served from the SkillHub cache and may be outdated.'}

{isRTL ? 'بررسی مخزن GitHub' : 'Check GitHub repository'}
)} {/* Stats Bar */}
{formatCompactNumber(skill.stars, locale)} {tCommon('stars')}
{formatCompactNumber(skill.downloads, locale)} {tCommon('downloads')}
{formatCompactNumber(skill.views, locale)} {tCommon('views')}
{/* Main Content */}
{/* Left: README Content */}
{/* Quick Install (Mobile) */}
{/* README Section */}

{getContentTitle(skill.sourceFormat)}

                      {skill.longDescription}
                    
{/* Source Links (Mobile) */}

{isRTL ? 'لینک‌ها' : 'Links'}

{/* Right: Sidebar (Desktop) */}
{/* Install Section */} {/* Links Card (Desktop) */} {skill.homepage && (

{isRTL ? 'لینک‌ها' : 'Links'}

{t('meta.homepage')}
)}
); }