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:
parent
e55ade5117
commit
898b320644
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'HumanLayer',
|
||||
url: 'www.humanlayer.dev',
|
||||
lang: 'en',
|
||||
};
|
||||
Loading…
Reference in New Issue