feat(route): add best of js route (#19641)

* feat(route): add best of js route

* Update lib/routes/bestofjs/monthly.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* Update lib/routes/bestofjs/monthly.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* Update lib/routes/bestofjs/monthly.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* fix(route): add lang property

* perf(route): add cache

* perf(route): use art-template

* fix(route): del cache

* fix: template syntax

* fix: remove duplicated cache

---------
This commit is contained in:
砖吐筷筷 2025-08-27 03:53:45 +08:00 committed by GitHub
parent 7f84ff2bab
commit a7a4c2e71e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 184 additions and 0 deletions

View File

@ -0,0 +1,137 @@
import path from 'node:path';
import { load } from 'cheerio';
import { Route, ViewType } from '@/types';
import ofetch from '@/utils/ofetch';
import cache from '@/utils/cache';
import { art } from '@/utils/render';
const BASEURL = 'https://bestofjs.org/rankings/monthly';
export const route: Route = {
path: '/rankings/monthly',
categories: ['programming'],
example: '/bestofjs/rankings/monthly',
view: ViewType.Notifications,
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['bestofjs.org/rankings/monthly/:year/:month'],
target: '/rankings/monthly',
},
],
name: 'Monthly Rankings',
maintainers: ['ztkuaikuai'],
url: 'bestofjs.org/rankings/monthly',
handler: async () => {
const targetMonths = getLastSixMonths();
const allNeededMonthlyRankings = await Promise.all(
targetMonths.map((data) => {
const [year, month] = data.split('-');
return getMonthlyRankings(year, month);
})
);
const items = allNeededMonthlyRankings.flatMap((oneMonthlyRankings, i) => {
const [year, month] = targetMonths[i].split('-');
const description = art(path.join(__dirname, 'templates/description.art'), { items: oneMonthlyRankings });
return {
title: `Best of JS Monthly Rankings - ${year}/${month}`,
description,
link: `${BASEURL}/${year}/${month}`,
guid: `${BASEURL}/${year}/${month}`,
author: 'Best of JS',
};
});
return {
title: 'Best of JS Monthly Rankings',
link: BASEURL,
description: 'Monthly rankings of the most popular JavaScript projects on Best of JS',
item: items,
language: 'en',
};
},
};
const getLastSixMonths = (): string[] => {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1; // 0-based to 1-based
return Array.from({ length: 6 }, (_, i) => {
let month = currentMonth - (i + 1);
let year = currentYear;
if (month <= 0) {
month += 12;
year -= 1;
}
return `${year}-${month}`;
});
};
interface RankingItem {
logo: string;
projectName: string;
githubLink: string;
homepageLink: string;
description: string;
tags: string[];
starCount: string;
additionalInfo: string;
}
const getMonthlyRankings = (year: string, month: string): Promise<RankingItem[]> => {
const targetUrl = `${BASEURL}/${year}/${month}`;
return cache.tryGet(targetUrl, async () => {
const response = await ofetch(targetUrl);
const $ = load(response);
return $('table.w-full tbody tr[data-testid="project-card"]')
.toArray()
.map((el) => {
const $tr = $(el);
// Project logo
const logo =
$tr
.find('td:first img')
.attr('src')
?.replace(/.dark./, '.') || '';
// Project name and link
const projectLink = $tr.find('td:nth-child(2) a[href^="/projects/"]').first();
const projectName = projectLink.text().trim();
// GitHub and homepage links
const githubLink = $tr.find('td:nth-child(2) a[href*="github.com"]').attr('href') || '';
const homepageLink = $tr.find('td:nth-child(2) a[href*="http"]:not([href*="github.com"])').attr('href') || '';
// Description
const description = $tr.find('td:nth-child(2) .font-serif').text().trim();
// Tags
const tags = $tr
.find('td:nth-child(2) [href*="/projects?tags="]')
.toArray()
.map((tag) => $(tag).text().trim());
// Star count
const starCount = $tr.find('td:nth-child(4) span:last').text().trim() || $tr.find('td:nth-child(2) .inline-flex span:last-child').text().trim();
// Additional info (contributors, created date)
const additionalInfo = $tr
.find('td:nth-child(3) > div')
.toArray()
.slice(1)
.map((el) => $(el).text().trim())
.join('; ');
return {
logo,
projectName,
githubLink,
homepageLink,
description,
tags,
starCount,
additionalInfo,
};
});
});
};

View File

@ -0,0 +1,11 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Best of JS',
url: 'bestofjs.org',
description: `
:::tip
Best of JS Monthly Rankings
:::`,
lang: 'en',
};

View File

@ -0,0 +1,36 @@
<ul>
{{each items item index}}
<li>
<p><strong>Rank {{index + 1}}</strong></p>
{{if item.logo}}
<img src="https://bestofjs.org{{item.logo}}" alt="{{item.projectName}}" height="32" width="32" />
{{/if}}
{{if item.projectName}}
<p><strong>Project:</strong> {{item.projectName}}</p>
{{/if}}
{{if item.description}}
<p>{{item.description}}</p>
{{/if}}
{{if item.starCount}}
<p><strong>Stars:</strong> {{item.starCount}}</p>
{{/if}}
{{if item.additionalInfo}}
<p><strong>Additional Info:</strong> {{item.additionalInfo}}</p>
{{/if}}
{{if item.githubLink}}
<p><strong>GitHub:</strong> <a href="{{item.githubLink}}">{{item.githubLink}}</a></p>
{{/if}}
{{if item.homepageLink}}
<p><strong>Homepage:</strong> <a href="{{item.homepageLink}}">{{item.homepageLink}}</a></p>
{{/if}}
{{if item.tags && item.tags.length}}
<p><strong>Tags:</strong>
{{each item.tags tag tIndex}}
<a href="/projects?tags={{tag.toLowerCase()}}">{{tag}}</a>{{if tIndex < item.tags.length - 1}}, {{/if}}
{{/each}}
</p>
{{/if}}
</li>
<br />
{{/each}}
</ul>