Skip to content

Commit f8d9c61

Browse files
zj1123581321HAPIclaude
committed
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 <noreply@hapi.run> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a2861cd commit f8d9c61

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

lib/routes/humanlayer/blog.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { load } from 'cheerio';
2+
3+
import type { DataItem, Route } from '@/types';
4+
import cache from '@/utils/cache';
5+
import ofetch from '@/utils/ofetch';
6+
import { parseDate } from '@/utils/parse-date';
7+
8+
export const route: Route = {
9+
path: '/blog',
10+
categories: ['blog'],
11+
example: '/humanlayer/blog',
12+
parameters: {},
13+
features: {
14+
requireConfig: false,
15+
requirePuppeteer: false,
16+
antiCrawler: false,
17+
supportBT: false,
18+
supportPodcast: false,
19+
supportScihub: false,
20+
},
21+
radar: [
22+
{
23+
source: ['www.humanlayer.dev/blog'],
24+
target: '/humanlayer/blog',
25+
},
26+
],
27+
name: 'Blog',
28+
maintainers: ['zj1123581321'],
29+
handler,
30+
url: 'www.humanlayer.dev/blog',
31+
};
32+
33+
async function handler(ctx) {
34+
const baseUrl = 'https://www.humanlayer.dev';
35+
const listUrl = `${baseUrl}/blog`;
36+
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
37+
38+
const response = await ofetch(listUrl);
39+
const $ = load(response);
40+
41+
const list = $('a.block.py-2.group[href^="/blog/"]')
42+
.toArray()
43+
.filter((el) => {
44+
const href = $(el).attr('href')!;
45+
return !href.startsWith('/blog/tags/');
46+
})
47+
.slice(0, limit)
48+
.map((el) => {
49+
const $el = $(el);
50+
const href = $el.attr('href')!;
51+
const title = $el.find('h2').text().trim();
52+
const metaLine = $el.find('p.text-sm').text().trim();
53+
const description = $el.find('p[style]').text().trim();
54+
55+
// meta format: "Author · Date · Read time · #tag1 #tag2"
56+
const parts = metaLine.split('·').map((s) => s.trim());
57+
const author = parts[0] || '';
58+
const dateStr = parts[1] || '';
59+
60+
return {
61+
title,
62+
link: `${baseUrl}${href}`,
63+
author,
64+
description,
65+
pubDate: dateStr ? parseDate(dateStr) : undefined,
66+
} as DataItem;
67+
});
68+
69+
const items = (await Promise.all(
70+
list.map((item) =>
71+
cache.tryGet(item.link!, async () => {
72+
const resp = await ofetch(item.link!);
73+
const $detail = load(resp);
74+
75+
const ogTitle = $detail('meta[property="og:title"]').attr('content');
76+
const ogDesc = $detail('meta[property="og:description"]').attr('content');
77+
const publishedTime = $detail('meta[property="article:published_time"]').attr('content');
78+
const ogAuthor = $detail('meta[property="article:author"]').attr('content');
79+
const ogImage = $detail('meta[property="og:image"]').attr('content');
80+
81+
const content = $detail('div.prose').html();
82+
83+
return {
84+
...item,
85+
title: ogTitle || item.title,
86+
description: content || ogDesc || item.description,
87+
pubDate: publishedTime ? parseDate(publishedTime) : item.pubDate,
88+
author: ogAuthor || item.author,
89+
banner: ogImage,
90+
} as DataItem;
91+
})
92+
)
93+
)) as DataItem[];
94+
95+
return {
96+
title: 'HumanLayer Blog',
97+
link: listUrl,
98+
language: 'en',
99+
item: items,
100+
};
101+
}

lib/routes/humanlayer/namespace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Namespace } from '@/types';
2+
3+
export const namespace: Namespace = {
4+
name: 'HumanLayer',
5+
url: 'www.humanlayer.dev',
6+
lang: 'en',
7+
};

0 commit comments

Comments
 (0)