|
1 | | -import { load } from 'cheerio'; |
| 1 | +import MarkdownIt from 'markdown-it'; |
2 | 2 |
|
3 | 3 | import type { Data, DataItem, Route } from '@/types'; |
4 | 4 | import { ViewType } from '@/types'; |
5 | 5 | import cache from '@/utils/cache'; |
6 | 6 | import ofetch from '@/utils/ofetch'; |
7 | 7 |
|
| 8 | +const md = MarkdownIt({ |
| 9 | + html: true, |
| 10 | + linkify: true, |
| 11 | +}); |
| 12 | + |
8 | 13 | export const route: Route = { |
9 | 14 | path: '/blog', |
10 | 15 | categories: ['programming'], |
@@ -35,43 +40,76 @@ export const route: Route = { |
35 | 40 | async function handler() { |
36 | 41 | const rootUrl = 'https://manus.im/blog'; |
37 | 42 |
|
38 | | - const response = await ofetch(rootUrl); |
39 | | - const $ = load(response); |
| 43 | + const renderData = await ofetch(rootUrl, { |
| 44 | + headers: { |
| 45 | + RSC: '1', |
| 46 | + }, |
| 47 | + responseType: 'text', |
| 48 | + }); |
40 | 49 |
|
41 | | - const list: DataItem[] = $('div.mt-10.px-6 > a') |
42 | | - .toArray() |
43 | | - .map((item) => { |
44 | | - const element = $(item); |
45 | | - const link = new URL(String(element.attr('href')), rootUrl).href; |
46 | | - const title = String(element.find('h2').attr('title')); |
| 50 | + let blogList; |
| 51 | + const lines = renderData.split('\n'); |
| 52 | + for (const line of lines) { |
| 53 | + if (line.includes('{"blogList":{"$typeName"')) { |
| 54 | + const jsonStr = line.slice(Math.max(0, line.indexOf('{"blogList":{"$typeName"'))); |
| 55 | + const lastBrace = jsonStr.lastIndexOf('}'); |
| 56 | + try { |
| 57 | + const parsed = JSON.parse(jsonStr.slice(0, Math.max(0, lastBrace + 1))); |
| 58 | + blogList = parsed.blogList; |
| 59 | + break; |
| 60 | + } catch { |
| 61 | + // Ignore parse errors and try next line if any |
| 62 | + } |
| 63 | + } |
| 64 | + } |
47 | 65 |
|
48 | | - return { |
49 | | - link, |
50 | | - title, |
51 | | - }; |
52 | | - }); |
| 66 | + if (!blogList || !blogList.groups) { |
| 67 | + throw new Error('Failed to parse blogList from RSC data'); |
| 68 | + } |
| 69 | + |
| 70 | + const list: Array<DataItem & { _contentUrl?: string }> = blogList.groups.flatMap( |
| 71 | + (group) => |
| 72 | + group.blogs?.map((blog) => ({ |
| 73 | + title: blog.title, |
| 74 | + link: `https://manus.im/blog/${blog.recordUid}`, |
| 75 | + pubDate: new Date(blog.createdAt.seconds * 1000), |
| 76 | + description: blog.desc, |
| 77 | + category: [group.kindName], |
| 78 | + _contentUrl: blog.contentUrl, |
| 79 | + })) ?? [] |
| 80 | + ); |
53 | 81 |
|
54 | 82 | const items: DataItem[] = await Promise.all( |
55 | | - list.map((item) => |
56 | | - cache.tryGet(String(item.link), async () => { |
57 | | - const response = await ofetch(String(item.link)); |
58 | | - const $ = load(response); |
59 | | - const description: string = $('div.relative:nth-child(3)').html() ?? ''; |
60 | | - const pubDateText: string = $('div.gap-3:nth-child(1) > span:nth-child(2)').text().trim(); |
61 | | - const currentYear: number = new Date().getFullYear(); |
62 | | - const pubDate: Date = new Date(`${pubDateText} ${currentYear}`); |
| 83 | + list.map( |
| 84 | + (item) => |
| 85 | + cache.tryGet(String(item.link), async () => { |
| 86 | + const contentUrl = item._contentUrl; |
| 87 | + let description = String(item.description); |
| 88 | + if (contentUrl) { |
| 89 | + try { |
| 90 | + let contentText = await ofetch(contentUrl, { responseType: 'text' }); |
| 91 | + // Fix video embeds: Manus uses  which markdown-it renders as <img> |
| 92 | + contentText = contentText.replaceAll(/!\[.*?\]\((.+?\.(mp4|mov|webm))\)/gi, '<video controls preload="metadata"><source src="$1"></video>'); |
| 93 | + // Parse markdown to HTML |
| 94 | + description = md.render(contentText); |
| 95 | + } catch { |
| 96 | + // Fallback to description from list if fetch fails |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + // Remove the temporary property to avoid pollution |
| 101 | + delete item._contentUrl; |
63 | 102 |
|
64 | | - return { |
65 | | - ...item, |
66 | | - description, |
67 | | - pubDate, |
68 | | - }; |
69 | | - }) |
| 103 | + return { |
| 104 | + ...item, |
| 105 | + description, |
| 106 | + }; |
| 107 | + }) as Promise<DataItem> |
70 | 108 | ) |
71 | 109 | ); |
72 | 110 |
|
73 | 111 | return { |
74 | | - title: 'Manus', |
| 112 | + title: 'Manus Blog', |
75 | 113 | link: rootUrl, |
76 | 114 | item: items, |
77 | 115 | language: 'en', |
|
0 commit comments