From fc4e37500206ca362a857b0bd2b083e1477e9564 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 11 Jul 2023 17:35:20 +0800 Subject: [PATCH] feat: add sitemap and rss support --- package.json | 3 +- pnpm-lock.yaml | 16 +++++++ src/app/api/xlog/summary/route.ts | 2 + src/app/feed/route.tsx | 79 +++++++++++++++++++++++++++++++ src/app/sitemap/route.tsx | 36 ++++++++++++++ src/lib/helper.server.ts | 17 +++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/app/feed/route.tsx create mode 100644 src/app/sitemap/route.tsx create mode 100644 src/lib/helper.server.ts diff --git a/package.json b/package.json index 1e6bd94d88..07f45bb0cd 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ "shiki": "0.14.3", "socket.io-client": "4.7.1", "sonner": "0.6.1", - "tailwind-merge": "1.13.2" + "tailwind-merge": "1.13.2", + "xss": "1.0.14" }, "devDependencies": { "@iconify-json/material-symbols": "1.1.50", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4eb9aaa85..bf274222be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ dependencies: tailwind-merge: specifier: 1.13.2 version: 1.13.2 + xss: + specifier: 1.0.14 + version: 1.0.14 devDependencies: '@iconify-json/material-symbols': @@ -3515,6 +3518,10 @@ packages: engines: {node: '>=4'} hasBin: true + /cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + dev: false + /csstype@3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} dev: false @@ -8102,6 +8109,15 @@ packages: engines: {node: '>=0.4.0'} dev: false + /xss@1.0.14: + resolution: {integrity: sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==} + engines: {node: '>= 0.10.0'} + hasBin: true + dependencies: + commander: 2.20.3 + cssfilter: 0.0.10 + dev: false + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} diff --git a/src/app/api/xlog/summary/route.ts b/src/app/api/xlog/summary/route.ts index 241126effe..08cea59481 100644 --- a/src/app/api/xlog/summary/route.ts +++ b/src/app/api/xlog/summary/route.ts @@ -10,6 +10,8 @@ const headers = { export const runtime = 'edge' +export const revalidate = 60 * 60 // 1 hour + export const GET = async (req: NextRequest) => { const query = req.nextUrl.searchParams const cid = query.get('cid') diff --git a/src/app/feed/route.tsx b/src/app/feed/route.tsx new file mode 100644 index 0000000000..87453afdc3 --- /dev/null +++ b/src/app/feed/route.tsx @@ -0,0 +1,79 @@ +import Markdown from 'markdown-to-jsx' +import xss from 'xss' +import type { AggregateRoot } from '@mx-space/api-client' + +import { escapeXml } from '~/lib/helper.server' +import { getQueryClient } from '~/lib/query-client.server' +import { apiClient } from '~/lib/request' + +export const runtime = 'edge' +export const revalidate = 60 * 60 // 1 hour + +export async function GET() { + const ReactDOM = (await import('react-dom/server')).default + const queryClient = await getQueryClient() + + const { author, data, url } = await queryClient.fetchQuery({ + queryKey: ['rss'], + queryFn: async () => { + const path = apiClient.aggregate.proxy.feed.toString(true) + return fetch(path).then((res) => res.json()) + }, + }) + + const agg = await fetch(apiClient.aggregate.proxy.toString(true)).then( + (res) => res.json() as Promise, + ) + + const { title } = agg.seo + const { avatar } = agg.user + const now = new Date() + const xml = ` + + ${title} + + + + ${now.toISOString()} + ${xss(url)} + + ${author} + + Mix Space CMS + ${now.toISOString()} + zh-CN + + ${xss(avatar || '')} + ${title} + ${xss(url)} + + ${await Promise.all( + data.map(async (item: any) => { + return ` + ${escapeXml(item.title)} + + ${xss(item.link)} + ${item.created} + ${item.modified} + 该渲染由 Shiro API 生成,可能存在排版问题,最佳体验请前往:${xss(item.link)} +${ReactDOM.renderToString({item.text})} +

+ 看完了?说点什么呢 +

`} + ]]> +
+
+ ` + }), + ).then((res) => res.join(''))} +
` + + return new Response(xml, { + headers: { + 'Content-Type': 'application/xml', + }, + }) +} diff --git a/src/app/sitemap/route.tsx b/src/app/sitemap/route.tsx new file mode 100644 index 0000000000..c18d491e28 --- /dev/null +++ b/src/app/sitemap/route.tsx @@ -0,0 +1,36 @@ +import { getQueryClient } from '~/lib/query-client.server' +import { apiClient } from '~/lib/request' + +export const runtime = 'edge' +export const revalidate = 60 * 60 // 1 hour + +export const GET = async () => { + const queryClient = await getQueryClient() + + const { data } = await queryClient.fetchQuery({ + queryKey: ['sitemap'], + queryFn: async () => { + const path = apiClient.aggregate.proxy.sitemap.toString(true) + return fetch(path).then((res) => res.json()) + }, + }) + + const xml = ` + + ${data + .map( + (item: any) => ` + ${item.url} + ${item.publishedAt || 'N/A'} + `, + ) + + .join('')} + + `.trim() + return new Response(xml, { + headers: { + 'Content-Type': 'application/xml', + }, + }) +} diff --git a/src/lib/helper.server.ts b/src/lib/helper.server.ts new file mode 100644 index 0000000000..05adf0437b --- /dev/null +++ b/src/lib/helper.server.ts @@ -0,0 +1,17 @@ +export function escapeXml(unsafe: string) { + return unsafe.replace(/[<>&'"]/g, (c) => { + switch (c) { + case '<': + return '<' + case '>': + return '>' + case '&': + return '&' + case "'": + return ''' + case '"': + return '"' + } + return c + }) +}