Fork of Chiri for Astro for my blog
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: RSS content formatting

the3ash cb94b527 71e7ba80

+528 -21
+6
package.json
··· 19 19 "dependencies": { 20 20 "@astrojs/mdx": "^4.3.1", 21 21 "@astrojs/netlify": "^6.5.1", 22 + "@astrojs/rss": "^4.0.12", 22 23 "@astrojs/sitemap": "^3.4.1", 23 24 "@playform/inline": "^0.1.2", 24 25 "astro": "^5.12.0", 25 26 "astro-og-canvas": "^0.7.0", 26 27 "canvaskit-wasm": "^0.40.0", 27 28 "katex": "^0.16.22", 29 + "markdown-it": "^14.1.0", 28 30 "mdast-util-to-string": "^4.0.0", 31 + "node-html-parser": "^7.0.1", 29 32 "reading-time": "^1.5.0", 30 33 "rehype-katex": "^7.0.1", 31 34 "remark-directive": "^4.0.0", 32 35 "remark-math": "^6.0.0", 36 + "sanitize-html": "^2.17.0", 33 37 "sharp": "^0.34.3", 34 38 "unist-util-visit": "^5.0.0" 35 39 }, 36 40 "devDependencies": { 37 41 "@eslint/js": "^9.31.0", 42 + "@types/markdown-it": "^14.1.2", 43 + "@types/sanitize-html": "^2.16.0", 38 44 "@typescript-eslint/eslint-plugin": "^8.37.0", 39 45 "@typescript-eslint/parser": "^8.37.0", 40 46 "eslint": "^9.31.0",
+147
pnpm-lock.yaml
··· 14 14 '@astrojs/netlify': 15 15 specifier: ^6.5.1 16 16 version: 6.5.1(@types/node@24.0.14)(astro@5.12.0(@netlify/blobs@10.0.6)(@types/node@24.0.14)(jiti@2.4.2)(rollup@4.45.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(jiti@2.4.2)(rollup@4.45.1)(tsx@4.20.3)(yaml@2.8.0) 17 + '@astrojs/rss': 18 + specifier: ^4.0.12 19 + version: 4.0.12 17 20 '@astrojs/sitemap': 18 21 specifier: ^3.4.1 19 22 version: 3.4.1 ··· 32 35 katex: 33 36 specifier: ^0.16.22 34 37 version: 0.16.22 38 + markdown-it: 39 + specifier: ^14.1.0 40 + version: 14.1.0 35 41 mdast-util-to-string: 36 42 specifier: ^4.0.0 37 43 version: 4.0.0 44 + node-html-parser: 45 + specifier: ^7.0.1 46 + version: 7.0.1 38 47 reading-time: 39 48 specifier: ^1.5.0 40 49 version: 1.5.0 ··· 47 56 remark-math: 48 57 specifier: ^6.0.0 49 58 version: 6.0.0 59 + sanitize-html: 60 + specifier: ^2.17.0 61 + version: 2.17.0 50 62 sharp: 51 63 specifier: ^0.34.3 52 64 version: 0.34.3 ··· 57 69 '@eslint/js': 58 70 specifier: ^9.31.0 59 71 version: 9.31.0 72 + '@types/markdown-it': 73 + specifier: ^14.1.2 74 + version: 14.1.2 75 + '@types/sanitize-html': 76 + specifier: ^2.16.0 77 + version: 2.16.0 60 78 '@typescript-eslint/eslint-plugin': 61 79 specifier: ^8.37.0 62 80 version: 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) ··· 117 135 resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} 118 136 engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} 119 137 138 + '@astrojs/rss@4.0.12': 139 + resolution: {integrity: sha512-O5yyxHuDVb6DQ6VLOrbUVFSm+NpObulPxjs6XT9q3tC+RoKbN4HXMZLpv0LvXd1qdAjzVgJ1NFD+zKHJNDXikw==} 140 + 120 141 '@astrojs/sitemap@3.4.1': 121 142 resolution: {integrity: sha512-VjZvr1e4FH6NHyyHXOiQgLiw94LnCVY4v06wN/D0gZKchTMkg71GrAHJz81/huafcmavtLkIv26HnpfDq6/h/Q==} 122 143 ··· 1172 1193 '@types/katex@0.16.7': 1173 1194 resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} 1174 1195 1196 + '@types/linkify-it@5.0.0': 1197 + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} 1198 + 1199 + '@types/markdown-it@14.1.2': 1200 + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 1201 + 1175 1202 '@types/mdast@4.0.4': 1176 1203 resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} 1204 + 1205 + '@types/mdurl@2.0.0': 1206 + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} 1177 1207 1178 1208 '@types/mdx@2.0.13': 1179 1209 resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} ··· 1198 1228 1199 1229 '@types/retry@0.12.2': 1200 1230 resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} 1231 + 1232 + '@types/sanitize-html@2.16.0': 1233 + resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} 1201 1234 1202 1235 '@types/sax@1.2.7': 1203 1236 resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} ··· 2099 2132 2100 2133 fast-uri@3.0.6: 2101 2134 resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} 2135 + 2136 + fast-xml-parser@5.2.5: 2137 + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} 2138 + hasBin: true 2102 2139 2103 2140 fastq@1.19.1: 2104 2141 resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} ··· 2329 2366 hastscript@9.0.1: 2330 2367 resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} 2331 2368 2369 + he@1.2.0: 2370 + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 2371 + hasBin: true 2372 + 2332 2373 hosted-git-info@7.0.2: 2333 2374 resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} 2334 2375 engines: {node: ^16.14.0 || >=18.0.0} ··· 2338 2379 2339 2380 html-void-elements@3.0.0: 2340 2381 resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} 2382 + 2383 + htmlparser2@8.0.2: 2384 + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} 2341 2385 2342 2386 htmlparser2@9.1.0: 2343 2387 resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} ··· 2473 2517 resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} 2474 2518 engines: {node: '>=12'} 2475 2519 2520 + is-plain-object@5.0.0: 2521 + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} 2522 + engines: {node: '>=0.10.0'} 2523 + 2476 2524 is-stream@2.0.1: 2477 2525 resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 2478 2526 engines: {node: '>=8'} ··· 2599 2647 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 2600 2648 engines: {node: '>= 0.8.0'} 2601 2649 2650 + linkify-it@5.0.0: 2651 + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} 2652 + 2602 2653 listhen@1.9.0: 2603 2654 resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} 2604 2655 hasBin: true ··· 2672 2723 resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} 2673 2724 engines: {node: '>=16'} 2674 2725 2726 + markdown-it@14.1.0: 2727 + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} 2728 + hasBin: true 2729 + 2675 2730 markdown-table@3.0.4: 2676 2731 resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} 2677 2732 ··· 2745 2800 mdn-data@2.12.2: 2746 2801 resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} 2747 2802 2803 + mdurl@2.0.0: 2804 + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} 2805 + 2748 2806 merge-options@3.0.4: 2749 2807 resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} 2750 2808 engines: {node: '>=10'} ··· 2977 3035 node-gyp-build@4.8.4: 2978 3036 resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} 2979 3037 hasBin: true 3038 + 3039 + node-html-parser@7.0.1: 3040 + resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==} 2980 3041 2981 3042 node-mock-http@1.0.1: 2982 3043 resolution: {integrity: sha512-0gJJgENizp4ghds/Ywu2FCmcRsgBTmRQzYPZm61wy+Em2sBarSka0OhQS5huLBg6od1zkNpnWMCZloQDFVvOMQ==} ··· 3121 3182 parse-latin@7.0.0: 3122 3183 resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} 3123 3184 3185 + parse-srcset@1.0.2: 3186 + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} 3187 + 3124 3188 parse5@7.3.0: 3125 3189 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 3126 3190 ··· 3233 3297 pump@3.0.3: 3234 3298 resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} 3235 3299 3300 + punycode.js@2.3.1: 3301 + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} 3302 + engines: {node: '>=6'} 3303 + 3236 3304 punycode@2.3.1: 3237 3305 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 3238 3306 engines: {node: '>=6'} ··· 3416 3484 resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} 3417 3485 engines: {node: '>=10'} 3418 3486 3487 + sanitize-html@2.17.0: 3488 + resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} 3489 + 3419 3490 sass-formatter@0.7.9: 3420 3491 resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} 3421 3492 ··· 3562 3633 strip-json-comments@3.1.1: 3563 3634 resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 3564 3635 engines: {node: '>=8'} 3636 + 3637 + strnum@2.1.1: 3638 + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} 3565 3639 3566 3640 style-to-js@1.1.17: 3567 3641 resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} ··· 3690 3764 engines: {node: '>=14.17'} 3691 3765 hasBin: true 3692 3766 3767 + uc.micro@2.1.0: 3768 + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} 3769 + 3693 3770 ufo@1.6.1: 3694 3771 resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 3695 3772 ··· 4130 4207 '@astrojs/prism@3.3.0': 4131 4208 dependencies: 4132 4209 prismjs: 1.30.0 4210 + 4211 + '@astrojs/rss@4.0.12': 4212 + dependencies: 4213 + fast-xml-parser: 5.2.5 4214 + kleur: 4.1.5 4133 4215 4134 4216 '@astrojs/sitemap@3.4.1': 4135 4217 dependencies: ··· 5197 5279 5198 5280 '@types/katex@0.16.7': {} 5199 5281 5282 + '@types/linkify-it@5.0.0': {} 5283 + 5284 + '@types/markdown-it@14.1.2': 5285 + dependencies: 5286 + '@types/linkify-it': 5.0.0 5287 + '@types/mdurl': 2.0.0 5288 + 5200 5289 '@types/mdast@4.0.4': 5201 5290 dependencies: 5202 5291 '@types/unist': 3.0.3 5292 + 5293 + '@types/mdurl@2.0.0': {} 5203 5294 5204 5295 '@types/mdx@2.0.13': {} 5205 5296 ··· 5223 5314 5224 5315 '@types/retry@0.12.2': {} 5225 5316 5317 + '@types/sanitize-html@2.16.0': 5318 + dependencies: 5319 + htmlparser2: 8.0.2 5320 + 5226 5321 '@types/sax@1.2.7': 5227 5322 dependencies: 5228 5323 '@types/node': 17.0.45 ··· 6364 6459 6365 6460 fast-uri@3.0.6: {} 6366 6461 6462 + fast-xml-parser@5.2.5: 6463 + dependencies: 6464 + strnum: 2.1.1 6465 + 6367 6466 fastq@1.19.1: 6368 6467 dependencies: 6369 6468 reusify: 1.1.0 ··· 6693 6792 property-information: 7.1.0 6694 6793 space-separated-tokens: 2.0.2 6695 6794 6795 + he@1.2.0: {} 6796 + 6696 6797 hosted-git-info@7.0.2: 6697 6798 dependencies: 6698 6799 lru-cache: 10.4.3 ··· 6700 6801 html-escaper@3.0.3: {} 6701 6802 6702 6803 html-void-elements@3.0.0: {} 6804 + 6805 + htmlparser2@8.0.2: 6806 + dependencies: 6807 + domelementtype: 2.3.0 6808 + domhandler: 5.0.3 6809 + domutils: 3.2.2 6810 + entities: 4.5.0 6703 6811 6704 6812 htmlparser2@9.1.0: 6705 6813 dependencies: ··· 6833 6941 6834 6942 is-plain-obj@4.1.0: {} 6835 6943 6944 + is-plain-object@5.0.0: {} 6945 + 6836 6946 is-stream@2.0.1: {} 6837 6947 6838 6948 is-stream@3.0.0: {} ··· 6946 7056 prelude-ls: 1.2.1 6947 7057 type-check: 0.4.0 6948 7058 7059 + linkify-it@5.0.0: 7060 + dependencies: 7061 + uc.micro: 2.1.0 7062 + 6949 7063 listhen@1.9.0: 6950 7064 dependencies: 6951 7065 '@parcel/watcher': 2.5.1 ··· 7025 7139 map-obj@5.0.2: {} 7026 7140 7027 7141 markdown-extensions@2.0.0: {} 7142 + 7143 + markdown-it@14.1.0: 7144 + dependencies: 7145 + argparse: 2.0.1 7146 + entities: 4.5.0 7147 + linkify-it: 5.0.0 7148 + mdurl: 2.0.0 7149 + punycode.js: 2.3.1 7150 + uc.micro: 2.1.0 7028 7151 7029 7152 markdown-table@3.0.4: {} 7030 7153 ··· 7230 7353 mdn-data@2.0.30: {} 7231 7354 7232 7355 mdn-data@2.12.2: {} 7356 + 7357 + mdurl@2.0.0: {} 7233 7358 7234 7359 merge-options@3.0.4: 7235 7360 dependencies: ··· 7608 7733 7609 7734 node-gyp-build@4.8.4: {} 7610 7735 7736 + node-html-parser@7.0.1: 7737 + dependencies: 7738 + css-select: 5.2.2 7739 + he: 1.2.0 7740 + 7611 7741 node-mock-http@1.0.1: {} 7612 7742 7613 7743 node-source-walk@7.0.1: ··· 7766 7896 unist-util-visit-children: 3.0.0 7767 7897 vfile: 6.0.3 7768 7898 7899 + parse-srcset@1.0.2: {} 7900 + 7769 7901 parse5@7.3.0: 7770 7902 dependencies: 7771 7903 entities: 6.0.1 ··· 7874 8006 dependencies: 7875 8007 end-of-stream: 1.4.5 7876 8008 once: 1.4.0 8009 + 8010 + punycode.js@2.3.1: {} 7877 8011 7878 8012 punycode@2.3.1: {} 7879 8013 ··· 8171 8305 8172 8306 safe-stable-stringify@2.5.0: {} 8173 8307 8308 + sanitize-html@2.17.0: 8309 + dependencies: 8310 + deepmerge: 4.3.1 8311 + escape-string-regexp: 4.0.0 8312 + htmlparser2: 8.0.2 8313 + is-plain-object: 5.0.0 8314 + parse-srcset: 1.0.2 8315 + postcss: 8.5.6 8316 + 8174 8317 sass-formatter@0.7.9: 8175 8318 dependencies: 8176 8319 suf-log: 2.5.3 ··· 8381 8524 8382 8525 strip-json-comments@3.1.1: {} 8383 8526 8527 + strnum@2.1.1: {} 8528 + 8384 8529 style-to-js@1.1.17: 8385 8530 dependencies: 8386 8531 style-to-object: 1.0.9 ··· 8502 8647 - supports-color 8503 8648 8504 8649 typescript@5.8.3: {} 8650 + 8651 + uc.micro@2.1.0: {} 8505 8652 8506 8653 ufo@1.6.1: {} 8507 8654
+92 -2
src/pages/atom.xml.ts
··· 1 + import rss from '@astrojs/rss' 2 + import { getCollection } from 'astro:content' 3 + import { getImage } from 'astro:assets' 4 + import { themeConfig } from '@/config' 1 5 import type { APIContext } from 'astro' 2 - import { generateAtom } from '@/utils/feed' 6 + import MarkdownIt from 'markdown-it' 7 + 8 + // Create markdown-it instance 9 + const md = new MarkdownIt({ 10 + html: true, 11 + linkify: true, 12 + typographer: true 13 + }) 3 14 4 15 export async function GET(context: APIContext) { 5 - return generateAtom(context) 16 + const posts = await getCollection('posts') 17 + const filteredPosts = posts.filter((post) => !post.id.startsWith('_')) 18 + const sortedPosts = filteredPosts.sort( 19 + (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() 20 + ) 21 + 22 + const items = await Promise.all( 23 + sortedPosts.map(async (post) => { 24 + // Process images in the post 25 + let content = post.body || '' 26 + 27 + // Find image references in the post 28 + const imageMatches = content.match(/!\[.*?\]\(([^)]+)\)/g) 29 + if (imageMatches) { 30 + for (const match of imageMatches) { 31 + const srcMatch = match.match(/!\[.*?\]\(([^)]+)\)/) 32 + if (srcMatch) { 33 + const originalSrc = srcMatch[1] 34 + // Try to process image using getImage 35 + try { 36 + // Build image path 37 + const imagePath = originalSrc.startsWith('./') 38 + ? originalSrc.substring(2) 39 + : originalSrc.startsWith('_assets/') 40 + ? originalSrc 41 + : `_assets/${originalSrc}` 42 + 43 + const image = await getImage({ 44 + src: `src/content/posts/${imagePath}`, 45 + width: 800, 46 + format: 'webp' 47 + }) 48 + 49 + // Replace image references in Markdown 50 + const fileName = 51 + imagePath 52 + .split('/') 53 + .pop() 54 + ?.replace(/\.[^/.]+$/, '') || 'image' 55 + const siteUrl = (context.site?.toString() || themeConfig.site.website).replace( 56 + /\/$/, 57 + '' 58 + ) 59 + const fullImageUrl = image.src.startsWith('http') 60 + ? image.src 61 + : `${siteUrl}${image.src}` 62 + content = content.replace( 63 + new RegExp( 64 + `!\\[.*?\\]\\(${originalSrc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 65 + 'g' 66 + ), 67 + `![${fileName}](${fullImageUrl})` 68 + ) 69 + } catch (error) { 70 + console.warn(`Failed to process image: ${originalSrc}`, error) 71 + } 72 + } 73 + } 74 + } 75 + 76 + // Convert Markdown to HTML 77 + const htmlContent = md.render(content) 78 + 79 + return { 80 + title: post.data.title, 81 + link: `/${post.id}/`, 82 + pubDate: post.data.pubDate, 83 + content: htmlContent 84 + } 85 + }) 86 + ) 87 + 88 + return rss({ 89 + title: themeConfig.site.title, 90 + description: themeConfig.site.description, 91 + site: context.site || themeConfig.site.website, 92 + items, 93 + customData: `<language>en-US</language>`, 94 + stylesheet: false 95 + }) 6 96 }
+91 -2
src/pages/rss.xml.ts
··· 1 + import rss from '@astrojs/rss' 2 + import { getCollection } from 'astro:content' 3 + import { getImage } from 'astro:assets' 4 + import { themeConfig } from '@/config' 1 5 import type { APIContext } from 'astro' 2 - import { generateRSS } from '@/utils/feed' 6 + import MarkdownIt from 'markdown-it' 7 + 8 + // Create markdown-it instance 9 + const md = new MarkdownIt({ 10 + html: true, 11 + linkify: true, 12 + typographer: true 13 + }) 3 14 4 15 export async function GET(context: APIContext) { 5 - return generateRSS(context) 16 + const posts = await getCollection('posts') 17 + const filteredPosts = posts.filter((post) => !post.id.startsWith('_')) 18 + const sortedPosts = filteredPosts.sort( 19 + (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() 20 + ) 21 + 22 + const items = await Promise.all( 23 + sortedPosts.map(async (post) => { 24 + // Process images in the post 25 + let content = post.body || '' 26 + 27 + // Find image references in the post 28 + const imageMatches = content.match(/!\[.*?\]\(([^)]+)\)/g) 29 + if (imageMatches) { 30 + for (const match of imageMatches) { 31 + const srcMatch = match.match(/!\[.*?\]\(([^)]+)\)/) 32 + if (srcMatch) { 33 + const originalSrc = srcMatch[1] 34 + // Try to process image using getImage 35 + try { 36 + // Build image path 37 + const imagePath = originalSrc.startsWith('./') 38 + ? originalSrc.substring(2) 39 + : originalSrc.startsWith('_assets/') 40 + ? originalSrc 41 + : `_assets/${originalSrc}` 42 + 43 + const image = await getImage({ 44 + src: `src/content/posts/${imagePath}`, 45 + width: 800, 46 + format: 'webp' 47 + }) 48 + 49 + // Replace image references in Markdown 50 + const fileName = 51 + imagePath 52 + .split('/') 53 + .pop() 54 + ?.replace(/\.[^/.]+$/, '') || 'image' 55 + const siteUrl = (context.site?.toString() || themeConfig.site.website).replace( 56 + /\/$/, 57 + '' 58 + ) 59 + const fullImageUrl = image.src.startsWith('http') 60 + ? image.src 61 + : `${siteUrl}${image.src}` 62 + content = content.replace( 63 + new RegExp( 64 + `!\\[.*?\\]\\(${originalSrc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 65 + 'g' 66 + ), 67 + `![${fileName}](${fullImageUrl})` 68 + ) 69 + } catch (error) { 70 + console.warn(`Failed to process image: ${originalSrc}`, error) 71 + } 72 + } 73 + } 74 + } 75 + 76 + // Convert Markdown to HTML 77 + const htmlContent = md.render(content) 78 + 79 + return { 80 + title: post.data.title, 81 + link: `/${post.id}/`, 82 + pubDate: post.data.pubDate, 83 + content: htmlContent 84 + } 85 + }) 86 + ) 87 + 88 + return rss({ 89 + title: themeConfig.site.title, 90 + description: themeConfig.site.description, 91 + site: context.site || themeConfig.site.website, 92 + items, 93 + customData: `<language>en-US</language>` 94 + }) 6 95 }
+192 -17
src/utils/feed.ts
··· 1 1 import { getCollection, type CollectionEntry } from 'astro:content' 2 2 import { themeConfig } from '@/config' 3 3 import type { APIContext } from 'astro' 4 + import MarkdownIt from 'markdown-it' 5 + import { parse } from 'node-html-parser' 6 + import sanitizeHtml from 'sanitize-html' 7 + 8 + // Dynamically import all post images 9 + const imageModules = import.meta.glob('/src/content/posts/_assets/*.(jpg|jpeg|png|gif|webp)', { 10 + eager: true, 11 + query: '?url', 12 + import: 'default' 13 + }) 14 + 15 + // Create image path mapping: filename -> URL 16 + const imagePathMap = new Map<string, string>() 17 + for (const [path, url] of Object.entries(imageModules)) { 18 + // Extract filename from path 19 + const fileName = path.split('/').pop() || '' 20 + imagePathMap.set(fileName, url as string) 21 + } 22 + 23 + // Create markdown-it instance with Astro-like configuration 24 + const md = new MarkdownIt({ 25 + html: true, 26 + linkify: true, 27 + typographer: true 28 + }) 29 + 30 + // Custom image renderer to preserve original paths 31 + md.renderer.rules.image = function (tokens, idx) { 32 + const token = tokens[idx] 33 + if (!token.attrs) return '' 34 + 35 + const srcIndex = token.attrIndex('src') 36 + const src = srcIndex >= 0 ? token.attrs[srcIndex][1] : '' 37 + const altIndex = token.attrIndex('alt') 38 + const alt = altIndex >= 0 ? token.attrs[altIndex][1] : '' 39 + 40 + return `<img src="${src}" alt="${alt}" />` 41 + } 42 + 43 + // Process image paths, convert relative paths to absolute paths 44 + function processImagePaths(html: string, siteUrl: string): string { 45 + const root = parse(html) 46 + 47 + // Process img tags 48 + root.querySelectorAll('img').forEach((img) => { 49 + const src = img.getAttribute('src') 50 + if (src && !src.startsWith('http') && !src.startsWith('//')) { 51 + // If it's a relative path, convert to absolute path 52 + let absoluteSrc: string 53 + if (src.startsWith('/')) { 54 + // Absolute path (relative to website root) 55 + absoluteSrc = `${siteUrl}${src}` 56 + } else if (src.startsWith('./_assets/')) { 57 + // Relative path (starting with ./_assets/) - use image mapping 58 + const fileName = src.substring(2) // Remove ./ 59 + const mappedUrl = imagePathMap.get(fileName.split('/').pop() || '') 60 + if (mappedUrl) { 61 + // Ensure mapped URL is absolute 62 + absoluteSrc = mappedUrl.startsWith('http') ? mappedUrl : `${siteUrl}${mappedUrl}` 63 + } else { 64 + // If mapping not found, use original path 65 + absoluteSrc = `${siteUrl}/${fileName}` 66 + } 67 + } else if (src.startsWith('_assets/')) { 68 + // Relative path (starting with _assets/) - use image mapping 69 + const fileName = src.split('/').pop() || '' 70 + const mappedUrl = imagePathMap.get(fileName) 71 + if (mappedUrl) { 72 + // Ensure mapped URL is absolute 73 + absoluteSrc = mappedUrl.startsWith('http') ? mappedUrl : `${siteUrl}${mappedUrl}` 74 + } else { 75 + // If mapping not found, use original path 76 + absoluteSrc = `${siteUrl}/${src}` 77 + } 78 + } else if (src.startsWith('./')) { 79 + // Other relative paths (starting with ./) 80 + const fileName = src.substring(2) // Remove ./ 81 + absoluteSrc = `${siteUrl}/_assets/${fileName}` 82 + } else { 83 + // Other relative paths 84 + absoluteSrc = `${siteUrl}/_assets/${src}` 85 + } 86 + img.setAttribute('src', absoluteSrc) 87 + } 88 + }) 89 + 90 + return root.toString() 91 + } 92 + 93 + // Clean and format HTML 94 + function sanitizeAndFormat(html: string): string { 95 + return sanitizeHtml(html, { 96 + allowedTags: [ 97 + 'h1', 98 + 'h2', 99 + 'h3', 100 + 'h4', 101 + 'h5', 102 + 'h6', 103 + 'p', 104 + 'br', 105 + 'hr', 106 + 'ul', 107 + 'ol', 108 + 'li', 109 + 'blockquote', 110 + 'pre', 111 + 'code', 112 + 'strong', 113 + 'em', 114 + 'del', 115 + 'mark', 116 + 'a', 117 + 'img', 118 + 'table', 119 + 'thead', 120 + 'tbody', 121 + 'tr', 122 + 'th', 123 + 'td', 124 + 'sub', 125 + 'sup', 126 + 'abbr', 127 + 'kbd', 128 + 'div', 129 + 'span' 130 + ], 131 + allowedAttributes: { 132 + '*': ['class', 'id'], 133 + a: ['href', 'title', 'target', 'rel'], 134 + img: ['src', 'alt', 'title', 'width', 'height'], 135 + abbr: ['title'], 136 + code: ['class'], 137 + pre: ['class'] 138 + }, 139 + allowedSchemes: ['http', 'https', 'mailto', 'tel'], 140 + transformTags: { 141 + a: (tagName: string, attribs: Record<string, string>) => { 142 + // Ensure external links open in a new window 143 + if (attribs.href && attribs.href.startsWith('http')) { 144 + return { 145 + tagName, 146 + attribs: { 147 + ...attribs, 148 + target: '_blank', 149 + rel: 'noopener noreferrer' 150 + } 151 + } 152 + } 153 + return { tagName, attribs } 154 + } 155 + } 156 + }) 157 + } 4 158 5 159 export async function generateRSS(context: APIContext) { 6 160 const posts = await getCollection('posts') ··· 9 163 (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => 10 164 b.data.pubDate.valueOf() - a.data.pubDate.valueOf() 11 165 ) 166 + 167 + // Ensure site URL doesn't end with slash 168 + const siteUrl = (context.site?.toString() || themeConfig.site.website).replace(/\/$/, '') 12 169 13 170 const rss = `<?xml version="1.0" encoding="UTF-8" ?> 14 171 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"> 15 172 <channel> 16 173 <title>${themeConfig.site.title}</title> 17 - <link>${context.site}</link> 174 + <link>${siteUrl}</link> 18 175 <description>${themeConfig.site.description}</description> 19 176 <language>zh-CN</language> 20 177 <lastBuildDate>${new Date().toUTCString()}</lastBuildDate> 21 - <atom:link href="${context.site}/rss.xml" rel="self" type="application/rss+xml" /> 178 + <atom:link href="${siteUrl}/rss.xml" rel="self" type="application/rss+xml" /> 22 179 ${sortedPosts 23 - .map( 24 - (post: CollectionEntry<'posts'>) => ` 180 + .map((post: CollectionEntry<'posts'>) => { 181 + // Convert Markdown to HTML 182 + const rawHtml = md.render(post.body || '') 183 + 184 + // Process image paths 185 + const processedHtml = processImagePaths(rawHtml, siteUrl) 186 + // Clean and format HTML 187 + const cleanHtml = sanitizeAndFormat(processedHtml) 188 + 189 + return ` 25 190 <item> 26 191 <title><![CDATA[${post.data.title}]]></title> 27 - <link>${context.site}/${post.id}/</link> 28 - <guid>${context.site}/${post.id}/</guid> 192 + <link>${siteUrl}/${post.id}/</link> 193 + <guid>${siteUrl}/${post.id}/</guid> 29 194 <pubDate>${post.data.pubDate.toUTCString()}</pubDate> 30 - <content:encoded><![CDATA[${post.body}]]></content:encoded> 195 + <content:encoded><![CDATA[${cleanHtml}]]></content:encoded> 31 196 </item> 32 197 ` 33 - ) 198 + }) 34 199 .join('')} 35 200 </channel> 36 201 </rss>` ··· 50 215 b.data.pubDate.valueOf() - a.data.pubDate.valueOf() 51 216 ) 52 217 218 + // Ensure site URL doesn't end with slash 219 + const siteUrl = (context.site?.toString() || themeConfig.site.website).replace(/\/$/, '') 220 + 53 221 const atom = `<?xml version="1.0" encoding="utf-8"?> 54 222 <feed xmlns="http://www.w3.org/2005/Atom"> 55 223 <title>${themeConfig.site.title}</title> 56 224 <subtitle>${themeConfig.site.description}</subtitle> 57 - <link href="${context.site}/atom.xml" rel="self" type="application/atom+xml" /> 58 - <link href="${context.site}" /> 59 - <id>${context.site}</id> 225 + <link href="${siteUrl}/atom.xml" rel="self" type="application/atom+xml" /> 226 + <link href="${siteUrl}" /> 227 + <id>${siteUrl}</id> 60 228 <updated>${new Date().toISOString()}</updated> 61 229 ${sortedPosts 62 - .map( 63 - (post: CollectionEntry<'posts'>) => ` 230 + .map((post: CollectionEntry<'posts'>) => { 231 + // Convert Markdown to HTML 232 + const rawHtml = md.render(post.body || '') 233 + // Process image paths 234 + const processedHtml = processImagePaths(rawHtml, siteUrl) 235 + // Clean and format HTML 236 + const cleanHtml = sanitizeAndFormat(processedHtml) 237 + 238 + return ` 64 239 <entry> 65 240 <title>${post.data.title}</title> 66 - <link href="${context.site}/${post.id}/" /> 67 - <id>${context.site}/${post.id}/</id> 241 + <link href="${siteUrl}/${post.id}/" /> 242 + <id>${siteUrl}/${post.id}/</id> 68 243 <published>${post.data.pubDate.toISOString()}</published> 69 - <content type="html"><![CDATA[${post.body}]]></content> 244 + <content type="html"><![CDATA[${cleanHtml}]]></content> 70 245 </entry> 71 246 ` 72 - ) 247 + }) 73 248 .join('')} 74 249 </feed>` 75 250