diff --git a/package.json b/package.json index b8caa55..22d0ff7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "fast-xml-parser": "^4.5.0", "googleapis": "^144.0.0", "gray-matter": "^4.0.3", + "markdown-it": "^14.1.0", "mdast-util-to-string": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -44,6 +45,7 @@ "reading-time": "^1.5.0", "rehype-external-links": "^3.0.0", "remark-unwrap-images": "^4.0.0", + "sanitize-html": "^2.13.1", "sharp": "^0.33.2", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", @@ -52,6 +54,7 @@ "devDependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/typography": "^0.5.10", + "@types/sanitize-html": "^2.13.0", "@typescript-eslint/parser": "^7.1.1", "dotenv": "^16.4.5", "eslint": "^8.57.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63ee1d6..62d75bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 @@ -83,6 +86,9 @@ importers: remark-unwrap-images: specifier: ^4.0.0 version: 4.0.1 + sanitize-html: + specifier: ^2.13.1 + version: 2.13.1 sharp: specifier: ^0.33.2 version: 0.33.5 @@ -102,6 +108,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.10 version: 0.5.15(tailwindcss@3.4.15) + '@types/sanitize-html': + specifier: ^2.13.0 + version: 2.13.0 '@typescript-eslint/parser': specifier: ^7.1.1 version: 7.18.0(eslint@8.57.1)(typescript@5.7.2) @@ -1130,6 +1139,9 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/sanitize-html@2.13.0': + resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} + '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -1636,6 +1648,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2214,6 +2230,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} @@ -2384,6 +2403,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -2543,6 +2566,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==} + lit-element@4.1.1: resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==} @@ -2610,6 +2636,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==} @@ -2670,6 +2700,9 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3002,6 +3035,9 @@ packages: parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -3227,6 +3263,10 @@ packages: pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + 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'} @@ -3463,6 +3503,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-html@2.13.1: + resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==} + sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} @@ -3760,6 +3803,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==} @@ -5186,6 +5232,10 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 + '@types/sanitize-html@2.13.0': + dependencies: + htmlparser2: 8.0.2 + '@types/sax@1.2.7': dependencies: '@types/node': 17.0.45 @@ -5840,6 +5890,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -6735,6 +6787,13 @@ snapshots: html-void-elements@3.0.0: {} + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 @@ -6878,6 +6937,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -7014,6 +7075,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lit-element@4.1.1: dependencies: '@lit-labs/ssr-dom-shim': 1.2.1 @@ -7091,6 +7156,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: {} mdast-util-definitions@6.0.0: @@ -7266,6 +7340,8 @@ snapshots: mdn-data@2.0.30: {} + mdurl@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.2: @@ -7772,6 +7848,8 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 + parse-srcset@1.0.2: {} + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -7923,6 +8001,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} qs@6.13.1: @@ -8290,6 +8370,15 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-html@2.13.1: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.49 + sass-formatter@0.7.9: dependencies: suf-log: 2.5.3 @@ -8653,6 +8742,8 @@ snapshots: typescript@5.7.2: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} ultrahtml@1.5.3: {} diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index e7e130a..d017600 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,33 +1,37 @@ -import { siteConfig } from '@/site-config' -import { getAllSortedPosts } from '@/utils' -import rss from '@astrojs/rss' -import type { APIContext } from 'astro' +import { siteConfig } from '@/site-config'; +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import type { CollectionEntry } from 'astro:content'; +import sanitizeHtml from 'sanitize-html'; + +const sanitizeText = (text: string | undefined) => text?.replace(/[\x00-\x1F\x7F-\x9F]/g, '') || ''; export async function GET(context: APIContext) { - const blog = await getAllSortedPosts() - return rss({ - // `` field in output xml - title: siteConfig.title, - // `<description>` field in output xml - description: siteConfig.description, - // Pull in your project "site" from the endpoint context - // https://docs.astro.build/en/reference/api-reference/#contextsite - site: context.site!, - // Array of `<item>`s in output xml - // See "Generating items" section for examples using content collections and glob imports - items: blog.map((post) => ({ - title: post.data.title, - pubDate: post.data.date, - description: post.data.description ?? '', - // Compute RSS link from post `slug` - // This example assumes all posts are rendered as `/blog/[slug]` routes - link: `/blog/${post.slug}/` - })), - // Add custom XML elements - customData: ` - <follow_challenge> - <feedId>83074007039123456</feedId> - <userId>62156866798228480</userId> - </follow_challenge>` - }) + const postImportResult = import.meta.glob<CollectionEntry<'post'>>('../content/post/**/*.md', { eager: true }); + const posts = Object.values(postImportResult); + const sortedPosts = posts.sort((a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf()); + + return rss({ + title: siteConfig.title, + description: siteConfig.description, + site: context.site!, + items: sortedPosts.map((post) => { + const prefix = post.data.category?.startsWith("日记-20") ? '/dairy/' : '/blog/'; + return { + title: sanitizeText(post.data.title), + pubDate: new Date(post.data.date), + description: sanitizeText(post.data.description), + link: `${prefix}${post.slug}`, + content: sanitizeHtml(post.body, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + allowedAttributes: { + ...sanitizeHtml.defaults.allowedAttributes, + img: ['src', 'alt'] + }, + textFilter: sanitizeText + }), + author: sanitizeText(post.data.author) + }; + }), + }); } diff --git a/src/site.config.ts b/src/site.config.ts index ad38163..b216005 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -1,5 +1,5 @@ -import type { SiteConfig } from '@/types' -import type { AstroExpressiveCodeOptions } from 'astro-expressive-code' +import type { SiteConfig } from '@/types'; +import type { AstroExpressiveCodeOptions } from 'astro-expressive-code'; export const siteConfig: SiteConfig = { // Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts) diff --git a/src/utils/index.ts b/src/utils/index.ts index 05e331d..67ac230 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,17 +1,13 @@ -export { cn } from './tailwind' -export { - getAllPosts, - getAllSortedPosts, - sortMDByDate, - getUniqueTags, - getUniqueTagsWithCount, - getAllCategories, - getUniqueCategories, - getUniqueCategoriesWithCount, - getallDiaries, - getallDiariesSorted -} from './post' export { getFormattedDate } from './date' +export { elementHasClass, rootInDarkMode, toggleClass } from './domElement' export { generateToc } from './generateToc' export type { TocItem } from './generateToc' -export { elementHasClass, toggleClass, rootInDarkMode } from './domElement' +export { + getAllCategories, getAllPosts, + getAllSortedPosts, getUniqueCategories, + getUniqueCategoriesWithCount, getUniqueTags, + getUniqueTagsWithCount, getAllDiaries as getallDiaries, + getAllDiariesSorted as getallDiariesSorted, sortMDByDate +} from './post' +export { cn } from './tailwind' + diff --git a/src/utils/post.ts b/src/utils/post.ts index dc7d1f8..6b03043 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -2,27 +2,27 @@ import type { CollectionEntry } from 'astro:content' import { getCollection } from 'astro:content' /** Note: this function filters out draft posts based on the environment */ -export async function getAllPosts() { - return await getCollection('post', ({ data }) => { +export async function getAllPosts(): Promise<CollectionEntry<'post'>[]> { + return await getCollection('post', ({ data }: { data: CollectionEntry<'post'> }) => { return !data.draft && !data.category?.startsWith('日记-20') }) } -export async function getAllSortedPosts() { +export async function getAllSortedPosts(): Promise<CollectionEntry<'post'>[]> { return sortMDByDate(await getAllPosts()) } -export const getallDiaries = async () => { - return await getCollection('post', ({ data }) => { +export const getAllDiaries = async (): Promise<CollectionEntry<'post'>[]> => { + return await getCollection('post', ({ data }: { data: CollectionEntry<'post'> }) => { return !data.draft && data.category?.startsWith('日记-20') }) } -export const getallDiariesSorted = async () => { - return sortMDByDate(await getallDiaries()) +export const getAllDiariesSorted = async (): Promise<CollectionEntry<'post'>[]> => { + return sortMDByDate(await getAllDiaries()) } -export function sortMDByDate(posts: Array<CollectionEntry<'post'>>) { +export function sortMDByDate(posts: Array<CollectionEntry<'post'>>): CollectionEntry<'post'>[] { return posts.sort((a, b) => { const aDate = new Date(a.data.date).valueOf() const bDate = new Date(b.data.date).valueOf() @@ -31,12 +31,12 @@ export function sortMDByDate(posts: Array<CollectionEntry<'post'>>) { } /** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */ -export function getAllTags(posts: Array<CollectionEntry<'post'>>) { +export function getAllTags(posts: Array<CollectionEntry<'post'>>): string[] { return posts.flatMap((post) => [...(post.data?.tags ?? [])]) } /** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */ -export function getUniqueTags(posts: Array<CollectionEntry<'post'>>) { +export function getUniqueTags(posts: Array<CollectionEntry<'post'>>): string[] { return [...new Set(getAllTags(posts))] } @@ -58,7 +58,7 @@ export function getAllCategories(posts: Array<CollectionEntry<'post'>>): string[ } /** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */ -export function getUniqueCategories(posts: Array<CollectionEntry<'post'>>) { +export function getUniqueCategories(posts: Array<CollectionEntry<'post'>>): string[] { return [...new Set(getAllCategories(posts))] } @@ -74,7 +74,7 @@ export function getUniqueCategoriesWithCount( ].sort((a, b) => b[1] - a[1]) } -export function getIdToSlugMap(posts: Array<CollectionEntry<'post'>>) { +export function getIdToSlugMap(posts: Array<CollectionEntry<'post'>>): Record<string, string> { return posts.reduce( (acc, post) => { acc[post.id.split('.md')[0]] = post.slug