From 898b3206440ce34df9ff9dbc4c9e9c1bccef48cd Mon Sep 17 00:00:00 2001 From: lex <33171229+zj1123581321@users.noreply.github.com> Date: Tue, 12 May 2026 00:03:51 +0800 Subject: [PATCH] feat(route): add /humanlayer/blog route (#21565) * feat(route): add /humanlayer/blog route Add RSS feed for HumanLayer blog (www.humanlayer.dev/blog), scraping blog listing and full article content with cheerio. via [HAPI](https://hapi.run) Co-Authored-By: HAPI Co-Authored-By: Claude Opus 4.6 (1M context) * refactor(route): use CSS :not() selector instead of JS filter for humanlayer blog Address PR review feedback: move /blog/tags/ exclusion into the CSS selector instead of using a separate .filter() call. via [HAPI](https://hapi.run) Co-Authored-By: HAPI Co-Authored-By: Claude Opus 4.6 (1M context) * fix(route): extract tags into category field for humanlayer blog Parse #-prefixed tags from the meta line and populate the category field. via [HAPI](https://hapi.run) Co-Authored-By: HAPI Co-Authored-By: Claude Opus 4.6 (1M context) * fix(route): address review feedback for humanlayer blog - Remove redundant :not() selector (tag links use different class) - Simplify tag extraction from parts.slice(3).join() to parts[3] - Fix tag regex to match hyphenated tags like best-practices via [HAPI](https://hapi.run) Co-Authored-By: HAPI Co-Authored-By: Claude Opus 4.6 (1M context) * fix(route): remove custom limit for humanlayer blog, use built-in via [HAPI](https://hapi.run) Co-Authored-By: HAPI Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: HAPI Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: zj1123581321 --- lib/routes/humanlayer/blog.ts | 97 ++++++++++++++++++++++++++++++ lib/routes/humanlayer/namespace.ts | 7 +++ 2 files changed, 104 insertions(+) create mode 100644 lib/routes/humanlayer/blog.ts create mode 100644 lib/routes/humanlayer/namespace.ts diff --git a/lib/routes/humanlayer/blog.ts b/lib/routes/humanlayer/blog.ts new file mode 100644 index 000000000..270ce63ea --- /dev/null +++ b/lib/routes/humanlayer/blog.ts @@ -0,0 +1,97 @@ +import { load } from 'cheerio'; + +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/blog', + categories: ['blog'], + example: '/humanlayer/blog', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.humanlayer.dev/blog'], + target: '/humanlayer/blog', + }, + ], + name: 'Blog', + maintainers: ['zj1123581321'], + handler, + url: 'www.humanlayer.dev/blog', +}; + +async function handler() { + const baseUrl = 'https://www.humanlayer.dev'; + const listUrl = `${baseUrl}/blog`; + + const response = await ofetch(listUrl); + const $ = load(response); + + const list = $('a.block.py-2.group[href^="/blog/"]') + .toArray() + .map((el) => { + const $el = $(el); + const href = $el.attr('href')!; + const title = $el.find('h2').text().trim(); + const metaLine = $el.find('p.text-sm').text().trim(); + const description = $el.find('p[style]').text().trim(); + + // meta format: "Author · Date · Read time · #tag1 #tag2" + const parts = metaLine.split('·').map((s) => s.trim()); + const author = parts[0] || ''; + const dateStr = parts[1] || ''; + const category = parts[3]?.match(/#[\w-]+/g)?.map((t) => t.slice(1)); + + return { + title, + link: `${baseUrl}${href}`, + author, + description, + pubDate: dateStr ? parseDate(dateStr) : undefined, + category, + } as DataItem; + }); + + const items = (await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const resp = await ofetch(item.link!); + const $detail = load(resp); + + const ogTitle = $detail('meta[property="og:title"]').attr('content'); + const ogDesc = $detail('meta[property="og:description"]').attr('content'); + const publishedTime = $detail('meta[property="article:published_time"]').attr('content'); + const ogAuthor = $detail('meta[property="article:author"]').attr('content'); + const ogImage = $detail('meta[property="og:image"]').attr('content'); + + const content = $detail('div.prose').html(); + + return { + ...item, + title: ogTitle || item.title, + description: content || ogDesc || item.description, + pubDate: publishedTime ? parseDate(publishedTime) : item.pubDate, + author: ogAuthor || item.author, + banner: ogImage, + } as DataItem; + }) + ) + )) as DataItem[]; + + return { + title: 'HumanLayer Blog', + link: listUrl, + language: 'en', + item: items, + }; +} diff --git a/lib/routes/humanlayer/namespace.ts b/lib/routes/humanlayer/namespace.ts new file mode 100644 index 000000000..9e5ffe1af --- /dev/null +++ b/lib/routes/humanlayer/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'HumanLayer', + url: 'www.humanlayer.dev', + lang: 'en', +};