From 5d3a3b03219f1f494aa3081f8402a1a5db07256a Mon Sep 17 00:00:00 2001 From: radishzzz Date: Tue, 14 Jan 2025 09:10:12 +0000 Subject: [PATCH] feat: add RSS feed generation and content management features --- pnpm-lock.yaml | 60 ++++++++++++++++++++++++++ src/components/Header.astro | 20 +++++++++ src/config/index.ts | 2 +- src/layouts/Layout.astro | 20 +++------ src/pages/rss.xml.ts | 51 +++++++++++++++++----- src/utils/content.config.ts | 85 +++++++++++++++++++++++++++---------- 6 files changed, 189 insertions(+), 49 deletions(-) create mode 100644 src/components/Header.astro diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03dddb1..7218b64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: astro-seo: specifier: ^0.8.4 version: 0.8.4(typescript@5.7.3) + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 overlayscrollbars: specifier: ^2.10.1 version: 2.10.1 @@ -99,6 +102,9 @@ importers: '@types/hast': specifier: ^3.0.4 version: 3.0.4 + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -1834,9 +1840,18 @@ packages: '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -3779,6 +3794,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@15.3.0: resolution: {integrity: sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A==} engines: {node: '>=18.12.0'} @@ -3867,6 +3885,10 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -3947,6 +3969,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4656,6 +4681,10 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -5355,6 +5384,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -7622,10 +7654,19 @@ snapshots: '@types/katex@0.16.7': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.13': {} '@types/ms@0.7.34': {} @@ -10182,6 +10223,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lint-staged@15.3.0: dependencies: chalk: 5.4.1 @@ -10287,6 +10332,15 @@ snapshots: markdown-extensions@2.0.0: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -10500,6 +10554,8 @@ snapshots: mdn-data@2.12.2: {} + mdurl@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -11377,6 +11433,8 @@ snapshots: property-information@6.5.0: {} + punycode.js@2.3.1: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -12314,6 +12372,8 @@ snapshots: typescript@5.7.3: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} ultrahtml@1.5.3: {} diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 0000000..dc03d7d --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,20 @@ +--- +import themeConfig from '@/config' + +const { title, subtitle, favicon } = themeConfig.site +--- + + + + + + + {title} - {subtitle} + + + diff --git a/src/config/index.ts b/src/config/index.ts index b4d0f50..1f84a1e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -73,7 +73,7 @@ export const themeConfig: ThemeConfig = { footer: { linkA: { name: 'RSS', - url: '#', + url: '/rss.xml', }, linkB: { name: 'Contact', diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 5d44e87..756bc18 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,20 +1,10 @@ +--- +import Header from '@/components/Header.astro' +--- + - - - - - - Astro Basics - - - - +
diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index e62a6e9..8aaaa87 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,33 +1,62 @@ import type { APIContext } from 'astro' +import type { CollectionEntry } from 'astro:content' import themeConfig from '@/config' import rss from '@astrojs/rss' import { getCollection } from 'astro:content' import MarkdownIt from 'markdown-it' import sanitizeHtml from 'sanitize-html' -import const parser = new MarkdownIt() const { title, description, url } = themeConfig.site const { language } = themeConfig.global +const followConfig = themeConfig.seo?.follow +// Extract first 100 chars from content as description +function getExcerpt(content: string): string { + const plainText = sanitizeHtml( + parser.render(content), + { + allowedTags: [], + allowedAttributes: {}, + }, + ) + + return `${plainText.slice(0, 100).trim()}...` +} + +// Generate RSS feed export async function GET(_context: APIContext) { - const posts = await getCollection('blog', ({ data }) => { - return !data.draft // 只包含非草稿文章 - }) + const posts = await getCollection( + 'posts', + ({ data }: { data: CollectionEntry<'posts'>['data'] }) => !data.draft, + ) return rss({ title, description, site: url, - items: posts.map(post => ({ + stylesheet: '/rss/styles.xsl', + items: posts.map((post: CollectionEntry<'posts'>) => ({ title: post.data.title, - pubDate: post.data.published, // 使用 published 而不是 pubDate - description: post.data.description, + pubDate: post.data.published, + description: post.data.description || getExcerpt(post.body), link: `/posts/${post.slug}/`, - content: sanitizeHtml(parser.render(post.body), { - allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), - }), + content: sanitizeHtml( + parser.render(post.body), + { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + }, + ), })), - customData: `${language}`, + customData: ` + ${language} + ${followConfig?.feedID && followConfig?.userID + ? ` + ${followConfig.feedID} + ${followConfig.userID} + ` + : '' + } + `.trim(), }) } diff --git a/src/utils/content.config.ts b/src/utils/content.config.ts index 3090423..556189c 100644 --- a/src/utils/content.config.ts +++ b/src/utils/content.config.ts @@ -1,50 +1,91 @@ import type { CollectionEntry } from 'astro:content' import { getCollection } from 'astro:content' -import MarkdownIt from 'markdown-it' -import sanitizeHtml, { type Tag } from 'sanitize-html' export type Post = CollectionEntry<'posts'> +export type PostData = Post['data'] +export type PostsGroupByYear = Map -export async function getPosts(isArchivePage = false) { - const posts = await getCollection('posts', ({ data }) => { - return import.meta.env.DEV || !data.draft - }) +// Get all posts and sort by publish date +export async function getPosts() { + const posts = await getCollection( + 'posts', + ({ data }: Post) => import.meta.env.DEV || !data.draft, + ) - // 按发布日期降序排序 - return posts.sort((a, b) => b.data.published.valueOf() - a.data.published.valueOf()) + return posts.sort((a: Post, b: Post) => + b.data.published.valueOf() - a.data.published.valueOf(), + ) } -// 获取所有标签及其对应的文章 -export async function getPostsByTags() { +// Group posts by tags +export async function sortPostsByTags() { const posts = await getPosts() const tagMap = new Map() - posts.forEach((post) => { - post.data.tags?.forEach((tag) => { - if (!tagMap.has(tag)) { - tagMap.set(tag, []) - } - tagMap.get(tag)?.push(post) - }) + posts.forEach((post: Post) => { + if (post.data.tags && post.data.tags.length > 0) { + post.data.tags.forEach((tag: string) => { + if (!tagMap.has(tag)) { + tagMap.set(tag, []) + } + tagMap.get(tag)?.push(post) + }) + } }) return tagMap } -// 获取指定标签的所有文章 +// Get posts by specific tag export async function getPostsByTag(tag: string) { const posts = await getPosts() - return posts.filter(post => post.data.tags?.includes(tag)) + + return posts.filter((post: Post) => + post.data.tags?.includes(tag), + ) } -// 获取所有标签列表 +// Get all tags list export async function getAllTags() { const posts = await getPosts() const tags = new Set() - posts.forEach((post) => { - post.data.tags?.forEach(tag => tags.add(tag)) + posts.forEach((post: Post) => { + post.data.tags?.forEach((tag: string) => + tags.add(tag), + ) }) return Array.from(tags) } + +// Group posts by year and sort +export async function getPostsByYear(): Promise { + const posts = await getPosts() + const yearMap = new Map() + + // Group by year + posts.forEach((post: Post) => { + const year = post.data.published.getFullYear() + if (!yearMap.has(year)) { + yearMap.set(year, []) + } + yearMap.get(year)?.push(post) + }) + + // Sort posts within each year + yearMap.forEach((yearPosts) => { + yearPosts.sort((a: Post, b: Post) => { + const aDate = a.data.published + const bDate = b.data.published + const monthDiff = bDate.getMonth() - aDate.getMonth() + + if (monthDiff !== 0) { + return monthDiff + } + return bDate.getDate() - aDate.getDate() + }) + }) + + return new Map([...yearMap.entries()].sort((a, b) => b[0] - a[0])) +}