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 <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 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 <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 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 <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 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 <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(route): remove custom limit for humanlayer blog, use built-in

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: zj1123581321 <zj1123581321@users.noreply.github.com>
This commit is contained in:
lex 2026-05-12 00:03:51 +08:00 committed by GitHub
parent e55ade5117
commit 898b320644
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 104 additions and 0 deletions

View File

@ -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,
};
}

View File

@ -0,0 +1,7 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HumanLayer',
url: 'www.humanlayer.dev',
lang: 'en',
};