SKILL.md
唯讀
名稱
github-trending
描述
取得並顯示 GitHub 熱門倉庫與開發者。適用於建立熱門倉庫儀表板、探索熱門專案或追蹤 GitHub 趨勢。觸發詞:GitHub trending、trending repos、popular repositories、GitHub discover。
GitHub 熱門資料
存取 GitHub 熱門倉庫與開發者資料。
重要說明
GitHub 並未提供官方熱門 API。 必須直接爬取 github.com/trending 頁面,或使用 GitHub Search API 作為替代方案。
方法一:直接網頁爬取(建議)
使用 Cheerio 直接爬取 github.com/trending:
import * as cheerio from 'cheerio';
interface TrendingRepo {
owner: string;
name: string;
fullName: string;
url: string;
description: string;
language: string;
languageColor: string;
stars: number;
forks: number;
starsToday: number;
}
async function scrapeTrending(options: {
language?: string;
since?: 'daily' | 'weekly' | 'monthly';
} = {}): Promise<TrendingRepo[]> {
// 建立 URL:github.com/trending 或 github.com/trending/typescript?since=weekly
let url = 'https://github.com/trending';
if (options.language) {
url += `/${encodeURIComponent(options.language)}`;
}
if (options.since) {
url += `?since=${options.since}`;
}
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; TrendingBot/1.0)',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch trending: ${response.status}`);
}
const html = await response.text();
const $ = cheerio.load(html);
const repos: TrendingRepo[] = [];
// 每個熱門倉庫在 article.Box-row 元素中
$('article.Box-row').each((_, element) => {
const $el = $(element);
// 取得倉庫連結(例如 /owner/repo)
const repoLink = $el.find('h2 a').attr('href')?.trim() || '';
const [, owner, name] = repoLink.split('/');
// 取得描述
const description = $el.find('p.col-9').text().trim();
// 取得程式語言
const language = $el.find('[itemprop="programmingLanguage"]').text().trim();
// 從顏色圓點取得語言顏色
const langColorStyle = $el.find('.repo-language-color').attr('style') || '';
const langColorMatch = langColorStyle.match(/background-color:\s*([^;]+)/);
const languageColor = langColorMatch ? langColorMatch[1].trim() : '';
// 取得星星數(總數)
const starsText = $el.find('a[href$="/stargazers"]').text().trim();
const stars = parseNumber(starsText);
// 取得 fork 數
const forksText = $el.find('a[href$="/forks"]').text().trim();
const forks = parseNumber(forksText);
// 取得今日/本週/本月星星數
const starsTodayText = $el.find('.float-sm-right, .d-inline-block.float-sm-right').text().trim();
const starsToday = parseNumber(starsTodayText);
if (owner && name) {
repos.push({
owner,
name,
fullName: `${owner}/${name}`,
url: `https://github.com${repoLink}`,
description,
language,
languageColor,
stars,
forks,
starsToday,
});
}
});
return repos;
}
function parseNumber(text: string): number {
const clean = text.replace(/,/g, '').trim();
if (clean.includes('k')) {
return Math.round(parseFloat(clean) * 1000);
}
return parseInt(clean) || 0;
}
方法二:GitHub Search API(官方替代方案)
使用 GitHub 的 Search API 尋找近期建立且星星數高的倉庫:
interface GitHubSearchResult {
total_count: number;
items: GitHubRepo[];
}
interface GitHubRepo {
full_name: string;
html_url: string;
description: string;
language: string;
stargazers_count: number;
forks_count: number;
created_at: string;
}
async function getTrendingViaSearch(options: {
language?: string;
days?: number;
minStars?: number;
} = {}): Promise<GitHubRepo[]> {
const days = options.days || 7;
const minStars = options.minStars || 100;
// 計算 N 天前的日期
const date = new Date();
date.setDate(date.getDate() - days);
const since = date.toISOString().split('T')[0];
// 建立搜尋查詢
const queryParts = [
`created:>${since}`,
`stars:>=${minStars}`,
];
if (options.language) {
queryParts.push(`language:${options.language}`);
}
const query = queryParts.join(' ');
const response = await fetch(
`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=25`,
{
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, // 選填但建議使用
'User-Agent': 'TrendingApp/1.0',
},
}
);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const data: GitHubSearchResult = await response.json();
return data.items;
}
注意: Search API 有速率限制(未認證每分鐘 10 次,有 token 每分鐘 30 次)。
Next.js API 路由(伺服端爬取)
// app/api/trending/route.ts
import { NextRequest } from 'next/server';
import * as cheerio from 'cheerio';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const language = searchParams.get('language') || '';
const since = searchParams.get('since') || 'daily';
try {
let url = 'https://github.com/trending';
if (language) url += `/${encodeURIComponent(language)}`;
url += `?since=${since}`;
const response = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0 (compatible)' },
next: { revalidate: 3600 }, // 快取 1 小時
});
const html = await response.text();
const repos = parseGitHubTrending(html);
return Response.json(repos);
} catch (error) {
console.error('Trending scrape failed:', error);
return Response.json(
{ error: 'Failed to fetch trending repos' },
{ status: 500 }
);
}
}
function parseGitHubTrending(html: string) {
const $ = cheerio.load(html);
const repos: any[] = [];
$('article.Box-row').each((_, el) => {
const $el = $(el);
const repoLink = $el.find('h2 a').attr('href') || '';
const [, owner, name] = repoLink.split('/');
repos.push({
owner,
name,
fullName: `${owner}/${name}`,
url: `https://github.com${repoLink}`,
description: $el.find('p.col-9').text().trim(),
language: $el.find('[itemprop="programmingLanguage"]').text().trim(),
stars: parseNumber($el.find('a[href$="/stargazers"]').text()),
forks: parseNumber($el.find('a[href$="/forks"]').text()),
starsToday: parseNumber($el.find('.float-sm-right').text()),
});
});
return repos;
}
function parseNumber(text: string): number {
const clean = text.replace(/,/g, '').trim();
if (clean.includes('k')) return Math.round(parseFloat(clean) * 1000);
return parseInt(clean) || 0;
}
React Hook
import { useState, useEffect } from 'react';
function useTrending(options: { language?: string; since?: string } = {}) {
const [repos, setRepos] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchTrending() {
setIsLoading(true);
try {
const params = new URLSearchParams();
if (options.language) params.set('language', options.language);
if (options.since) params.set('since', options.since);
const response = await fetch(`/api/trending?${params}`);
if (!response.ok) throw new Error('Failed to fetch');
setRepos(await response.json());
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}
fetchTrending();
}, [options.language, options.since]);
return { repos, isLoading, error };
}
重要考量
- 無官方 API:GitHub 的熱門頁面沒有官方 API,爬取是唯一選項
- 速率限制:尊重 GitHub 伺服器,積極使用快取
- HTML 結構變更:GitHub 可能更改 HTML,請監控是否失效
- User-Agent:務必包含 User-Agent 標頭
- 僅限伺服端:在伺服端進行爬取以避免 CORS 問題
資源
- GitHub 熱門頁面:https://github.com/trending
- GitHub Search API:https://docs.github.com/en/rest/search




