diff --git a/package.json b/package.json index 2cc154cc..5d5c1bbe 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "remark-rehype": "11.1.0", "remark-stringify": "11.0.0", "remove-markdown": "0.5.0", + "rss-parser": "3.13.0", "serialize-javascript": "6.0.2", "sharp": "0.32.6", "sortablejs": "1.15.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c101b27a..29faa7db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -351,6 +351,9 @@ dependencies: remove-markdown: specifier: 0.5.0 version: 0.5.0 + rss-parser: + specifier: 3.13.0 + version: 3.13.0 serialize-javascript: specifier: 6.0.2 version: 6.0.2 @@ -7424,6 +7427,10 @@ packages: tapable: 2.2.1 dev: true + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: false + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -12852,6 +12859,13 @@ packages: resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} dev: false + /rss-parser@3.13.0: + resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} + dependencies: + entities: 2.2.0 + xml2js: 0.5.0 + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -12911,6 +12925,10 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: false + /saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -14651,6 +14669,19 @@ packages: engines: {node: '>=18'} dev: false + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + /xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} diff --git a/src/app/api/rss-parser/route.ts b/src/app/api/rss-parser/route.ts new file mode 100644 index 00000000..8a063d00 --- /dev/null +++ b/src/app/api/rss-parser/route.ts @@ -0,0 +1,37 @@ +import Parser from "rss-parser" + +import { cacheGet } from "~/lib/redis.server" +import { getQuery, NextServerResponse } from "~/lib/server-helper" + +const parser = new Parser() + +export async function GET(req: Request): Promise { + let { url } = getQuery(req) + const res = new NextServerResponse() + + if (!url) { + return res.status(400).json({ error: "Bad request." }) + } + + try { + const parsed = (await cacheGet({ + key: `rss-parser/${url}`, + expireTime: 60 * 60 * 1, + getValueFun: async () => { + let feed: Parser.Output<{ + [key: string]: any + }> | null = null + try { + feed = await parser.parseURL(url) + } catch (error) {} + return feed + }, + })) as Parser.Output<{ + [key: string]: any + }> | null + return res.status(parsed ? 200 : 404).json({ data: parsed ?? null }) + } catch (error: any) { + console.error(error) + return res.status(400).json({ error: error?.message ?? "Bad request." }) + } +} diff --git a/src/components/ui/RSS.tsx b/src/components/ui/RSS.tsx new file mode 100644 index 00000000..18ac9bba --- /dev/null +++ b/src/components/ui/RSS.tsx @@ -0,0 +1,84 @@ +import Parser from "rss-parser" + +import { Image } from "~/components/ui/Image" +import dayjs from "~/lib/dayjs" +import { SITE_URL } from "~/lib/env" + +function extractImgSrc(htmlString: string) { + const regex = /]+src="([^">]+)"/g + const matches = htmlString.match(regex) + if (matches?.[0]) { + const src = matches[0].match(/src="([^">]+)"/)?.[1] + return src + } + return null +} + +export default async function RSS({ + src, + limit, +}: { + src: string + limit?: number +}) { + let feed: Parser.Output<{ + [key: string]: any + }> | null = null + + if (src) { + try { + feed = ( + await (await fetch(`${SITE_URL}/api/rss-parser?url=${src}`)).json() + ).data + } catch (error) {} + } + + if (limit && feed?.items) { + feed = { + ...feed, + items: feed.items.slice(0, +limit), + } + } + + return ( +
+ {feed?.items?.map((item) => { + const img = extractImgSrc(item.content || item.description || "") + console.log(img, img?.startsWith("https://")) + + return ( + + {img?.startsWith("https://") && ( + {item.title + )} + + {item.title} + + {item.contentSnippet} + + + {dayjs + .duration( + dayjs(item.isoDate).diff(dayjs(), "minute"), + "minute", + ) + .humanize()}{" "} + ago + + + + ) + })} +
+ ) +} diff --git a/src/markdown/index.ts b/src/markdown/index.ts index 8e6d0af9..5d13c2b8 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -60,6 +60,7 @@ const GithubRepo = dynamic(() => import("~/components/ui/GithubRepo")) const XLogPost = dynamic(() => import("~/components/ui/XLogPost")) const APlayer = dynamic(() => import("~/components/ui/APlayer")) const DPlayer = dynamic(() => import("~/components/ui/DPlayer")) +const RSS = dynamic(() => import("~/components/ui/RSS")) refractor.alias("html", ["svelte", "vue"]) refractor.register(langTsx) @@ -169,6 +170,7 @@ export const renderPageContent = (content: string, strictMode?: boolean) => { "xlog-post": XLogPost, // @ts-expect-error style: Style, + rss: RSS, }, ignoreInvalidStyle: true, jsx, diff --git a/src/markdown/sanitize-schema.ts b/src/markdown/sanitize-schema.ts index 2e599470..16f1608a 100644 --- a/src/markdown/sanitize-schema.ts +++ b/src/markdown/sanitize-schema.ts @@ -14,6 +14,7 @@ const scheme = deepmerge(defaultSchema, { "source", "mermaid", "aside", + "rss", ...["svg", "path", "circle"], ], attributes: { @@ -45,6 +46,7 @@ const scheme = deepmerge(defaultSchema, { ], path: ["d", "fill"], circle: ["cx", "cy", "r", "fill"], + rss: ["src", "limit"], }, protocols: { href: ["magnet", "ed2k"],