From 6e2ca6318964da05fe24d1609346afd405d4aed3 Mon Sep 17 00:00:00 2001 From: Lipeng <139090667+superguyDiluc@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:41:55 +0800 Subject: [PATCH] feat(route): add Meritalk latest articles (#21282) * feat: add route and namespace for MeriTalk articles * feat: update articles route to support pagination and add description rendering * feat: enhance articles route with improved description and error handling * feat(meritalk): add articles route with pagination limit - Add latest articles route for Meritalk - Update route example and parameter description - Implement pagination limit to avoid Cloudflare anti-bot * fix(meritalk): remove nums param and fetch only first page * fix(meritalk): update route category to new-media Co-authored-by: Tony * fix(meritalk): remove the post-processing of items and modify some of the logic in the handler - Remove unnecessary sorting. - Remove custom limit logic. --------- --- lib/routes/meritalk/articles.ts | 80 +++++++++++++++++++ lib/routes/meritalk/namespace.ts | 6 ++ lib/routes/meritalk/templates/description.tsx | 21 +++++ 3 files changed, 107 insertions(+) create mode 100644 lib/routes/meritalk/articles.ts create mode 100644 lib/routes/meritalk/namespace.ts create mode 100644 lib/routes/meritalk/templates/description.tsx diff --git a/lib/routes/meritalk/articles.ts b/lib/routes/meritalk/articles.ts new file mode 100644 index 000000000..a602e2b50 --- /dev/null +++ b/lib/routes/meritalk/articles.ts @@ -0,0 +1,80 @@ +import { load } from 'cheerio'; + +import type { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +import { renderDescription } from './templates/description'; + +export const route: Route = { + path: '/articles', + categories: ['new-media'], + example: '/meritalk/articles', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['meritalk.com/articles/'], + target: '/articles', + }, + ], + name: 'Latest Articles', + maintainers: ['superguyDiluc'], + handler, +}; + +async function handler() { + const baseUrl = 'https://www.meritalk.com/articles'; + + const { data: response } = await got(baseUrl); + const $ = load(response); + + const list = $('div.news-block-sm') + .toArray() + .map((item) => { + const $item = $(item); + const a = $item.find('.news-block-title a'); + const link = a.attr('href'); + return { + title: a.text().trim(), + link: link as string, + pubDate: parseDate($item.find('time[datetime]').attr('datetime') as string), + category: $item + .find('.category-header-name a') + .toArray() + .map((elem) => $(elem).text()), + description: '', + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const { data: response } = await got(item.link); + const $ = load(response); + + const featuredImage = $('.single-featured-image').first().html() || ''; + const fullContent = $('.single-body').first().html() || ''; + item!.description = renderDescription({ + featuredImage, + fullContent, + }); + + return item; + }) + ) + ); + + return { + title: 'News – MeriTalk', + link: 'https://www.meritalk.com/articles/', + item: items, + }; +} diff --git a/lib/routes/meritalk/namespace.ts b/lib/routes/meritalk/namespace.ts new file mode 100644 index 000000000..793aef163 --- /dev/null +++ b/lib/routes/meritalk/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'MeriTalk', + url: 'meritalk.com', +}; diff --git a/lib/routes/meritalk/templates/description.tsx b/lib/routes/meritalk/templates/description.tsx new file mode 100644 index 000000000..f3b6d0045 --- /dev/null +++ b/lib/routes/meritalk/templates/description.tsx @@ -0,0 +1,21 @@ +import { raw } from 'hono/html'; +import { renderToString } from 'hono/jsx/dom/server'; + +type DescriptionParams = { + featuredImage?: string; + fullContent?: string; +}; + +export const renderDescription = ({ featuredImage, fullContent }: DescriptionParams) => + renderToString( + <> + {featuredImage ? ( + <> + {raw(featuredImage)} +
+ + ) : null} + + {fullContent ? raw(fullContent) : null} + + );