diff --git a/package.json b/package.json index 47bc730..3d95251 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "astro": "^5.12.0", "astro-og-canvas": "^0.7.0", "canvaskit-wasm": "^0.40.0", + "feed": "^5.1.0", "katex": "^0.16.22", "markdown-it": "^14.1.0", "mdast-util-to-string": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ec9ed9..08b0fc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: canvaskit-wasm: specifier: ^0.40.0 version: 0.40.0 + feed: + specifier: ^5.1.0 + version: 5.1.0 katex: specifier: ^0.16.22 version: 0.16.22 @@ -2154,6 +2157,10 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + feed@5.1.0: + resolution: {integrity: sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg==} + engines: {node: '>=20', pnpm: '>=10'} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -4039,6 +4046,10 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + xss@1.0.15: resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} engines: {node: '>= 0.10.0'} @@ -6477,6 +6488,10 @@ snapshots: fecha@4.2.3: {} + feed@5.1.0: + dependencies: + xml-js: 1.6.11 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -8882,6 +8897,10 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + xml-js@1.6.11: + dependencies: + sax: 1.4.1 + xss@1.0.15: dependencies: commander: 2.20.3 diff --git a/public/feeds/atom-style.xsl b/public/feeds/atom-style.xsl new file mode 100644 index 0000000..989d399 --- /dev/null +++ b/public/feeds/atom-style.xsl @@ -0,0 +1,92 @@ + + + + + + + <xsl:value-of select="/atom:feed/atom:title" /> - Atom Feed + + + + + +
+
+ + + + + + + + +
+
+
+ To subscribe, copy the URL from the address bar and add it to your feed reader. +
+ + + +
+ + + +
+
\ No newline at end of file diff --git a/public/feeds/rss-style.xsl b/public/feeds/rss-style.xsl new file mode 100644 index 0000000..a319466 --- /dev/null +++ b/public/feeds/rss-style.xsl @@ -0,0 +1,107 @@ + + + + + + + <xsl:value-of select="/rss/channel/title" /> - RSS Feed + + + + + +
+
+ + + + + + + + +
+
+
+ To subscribe, copy the URL from the address bar and add it to your feed reader. +
+ + + +
+ + + +
+
\ No newline at end of file diff --git a/scripts/toggle-proxy.ts b/scripts/toggle-proxy.ts index 20e555f..413bab6 100644 --- a/scripts/toggle-proxy.ts +++ b/scripts/toggle-proxy.ts @@ -26,24 +26,42 @@ const linkCardEnabled: boolean = match[1] === 'true' // Helper to comment/uncomment adapter lines in astro.config.ts function toggleAstroAdapter(comment: boolean) { const astroConfig = fs.readFileSync(astroConfigPath, 'utf-8').split('\n') - // 16: import netlify..., 19: adapter: netlify() (0-based) - const importIdx = 16 - const adapterIdx = 19 + + // Find the import line for netlify adapter (including commented lines) + const importIndex = astroConfig.findIndex( + (line) => line.trim().includes('import') && line.includes('netlify') + ) + + // Find the adapter line (including commented lines) + const adapterIndex = astroConfig.findIndex( + (line) => line.trim().includes('adapter:') && line.includes('netlify') + ) + + if (importIndex === -1 || adapterIndex === -1) { + console.error('Could not find netlify adapter import or configuration') + return + } + if (comment) { - if (!astroConfig[importIdx].trim().startsWith('//')) { - astroConfig[importIdx] = '// ' + astroConfig[importIdx] + // Comment out the import line if not already commented + if (!astroConfig[importIndex].trim().startsWith('//')) { + astroConfig[importIndex] = '// ' + astroConfig[importIndex] } - if (!astroConfig[adapterIdx].trim().startsWith('//')) { - astroConfig[adapterIdx] = '// ' + astroConfig[adapterIdx] + // Comment out the adapter line if not already commented + if (!astroConfig[adapterIndex].trim().startsWith('//')) { + astroConfig[adapterIndex] = '// ' + astroConfig[adapterIndex] } } else { - if (astroConfig[importIdx].trim().startsWith('//')) { - astroConfig[importIdx] = astroConfig[importIdx].replace(/^\/\/\s?/, '') + // Uncomment the import line if commented + if (astroConfig[importIndex].trim().startsWith('//')) { + astroConfig[importIndex] = astroConfig[importIndex].replace(/^\/\/\s?/, '') } - if (astroConfig[adapterIdx].trim().startsWith('//')) { - astroConfig[adapterIdx] = astroConfig[adapterIdx].replace(/^\/\/\s?/, '') + // Uncomment the adapter line if commented + if (astroConfig[adapterIndex].trim().startsWith('//')) { + astroConfig[adapterIndex] = astroConfig[adapterIndex].replace(/^\/\/\s?/, '') } } + fs.writeFileSync(astroConfigPath, astroConfig.join('\n'), 'utf-8') } @@ -59,8 +77,8 @@ if (!linkCardEnabled) { // If linkCard is enabled, restore proxy.ts and uncomment adapter if (fs.existsSync(backupPath)) { fs.renameSync(backupPath, proxyPath) - console.log('🔵 proxy.ts enabled') + console.log('🟢 proxy.ts enabled') } toggleAstroAdapter(false) - console.log('🔵 adapter config uncommented') + console.log('🟢 adapter config uncommented') } diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index ceb3f43..fa0f1ec 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -12,6 +12,7 @@ export async function getStaticPaths() { props: post })) } + type Props = CollectionEntry<'posts'> const post = Astro.props diff --git a/src/pages/atom.xml.ts b/src/pages/atom.xml.ts index d7638d1..3a9e5d2 100644 --- a/src/pages/atom.xml.ts +++ b/src/pages/atom.xml.ts @@ -1,96 +1,4 @@ -import rss from '@astrojs/rss' -import { getCollection } from 'astro:content' -import { getImage } from 'astro:assets' -import { themeConfig } from '@/config' +import { generateAtom } from '@/utils/feed' import type { APIContext } from 'astro' -import MarkdownIt from 'markdown-it' -// Create markdown-it instance -const md = new MarkdownIt({ - html: true, - linkify: true, - typographer: true -}) - -export async function GET(context: APIContext) { - const posts = await getCollection('posts') - const filteredPosts = posts.filter((post) => !post.id.startsWith('_')) - const sortedPosts = filteredPosts.sort( - (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() - ) - - const items = await Promise.all( - sortedPosts.map(async (post) => { - // Process images in the post - let content = post.body || '' - - // Find image references in the post - const imageMatches = content.match(/!\[.*?\]\(([^)]+)\)/g) - if (imageMatches) { - for (const match of imageMatches) { - const srcMatch = match.match(/!\[.*?\]\(([^)]+)\)/) - if (srcMatch) { - const originalSrc = srcMatch[1] - // Try to process image using getImage - try { - // Build image path - const imagePath = originalSrc.startsWith('./') - ? originalSrc.substring(2) - : originalSrc.startsWith('_assets/') - ? originalSrc - : `_assets/${originalSrc}` - - const image = await getImage({ - src: `src/content/posts/${imagePath}`, - width: 800, - format: 'webp' - }) - - // Replace image references in Markdown - const fileName = - imagePath - .split('/') - .pop() - ?.replace(/\.[^/.]+$/, '') || 'image' - const siteUrl = (context.site?.toString() || themeConfig.site.website).replace( - /\/$/, - '' - ) - const fullImageUrl = image.src.startsWith('http') - ? image.src - : `${siteUrl}${image.src}` - content = content.replace( - new RegExp( - `!\\[.*?\\]\\(${originalSrc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, - 'g' - ), - `![${fileName}](${fullImageUrl})` - ) - } catch (error) { - console.warn(`Failed to process image: ${originalSrc}`, error) - } - } - } - } - - // Convert Markdown to HTML - const htmlContent = md.render(content) - - return { - title: post.data.title, - link: `/${post.id}/`, - pubDate: post.data.pubDate, - content: htmlContent - } - }) - ) - - return rss({ - title: themeConfig.site.title, - description: themeConfig.site.description, - site: context.site || themeConfig.site.website, - items, - customData: `en-US`, - stylesheet: false - }) -} +export const GET = (context: APIContext) => generateAtom(context) diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index a4bc5b8..0cc8a11 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,95 +1,4 @@ -import rss from '@astrojs/rss' -import { getCollection } from 'astro:content' -import { getImage } from 'astro:assets' -import { themeConfig } from '@/config' +import { generateRSS } from '@/utils/feed' import type { APIContext } from 'astro' -import MarkdownIt from 'markdown-it' -// Create markdown-it instance -const md = new MarkdownIt({ - html: true, - linkify: true, - typographer: true -}) - -export async function GET(context: APIContext) { - const posts = await getCollection('posts') - const filteredPosts = posts.filter((post) => !post.id.startsWith('_')) - const sortedPosts = filteredPosts.sort( - (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() - ) - - const items = await Promise.all( - sortedPosts.map(async (post) => { - // Process images in the post - let content = post.body || '' - - // Find image references in the post - const imageMatches = content.match(/!\[.*?\]\(([^)]+)\)/g) - if (imageMatches) { - for (const match of imageMatches) { - const srcMatch = match.match(/!\[.*?\]\(([^)]+)\)/) - if (srcMatch) { - const originalSrc = srcMatch[1] - // Try to process image using getImage - try { - // Build image path - const imagePath = originalSrc.startsWith('./') - ? originalSrc.substring(2) - : originalSrc.startsWith('_assets/') - ? originalSrc - : `_assets/${originalSrc}` - - const image = await getImage({ - src: `src/content/posts/${imagePath}`, - width: 800, - format: 'webp' - }) - - // Replace image references in Markdown - const fileName = - imagePath - .split('/') - .pop() - ?.replace(/\.[^/.]+$/, '') || 'image' - const siteUrl = (context.site?.toString() || themeConfig.site.website).replace( - /\/$/, - '' - ) - const fullImageUrl = image.src.startsWith('http') - ? image.src - : `${siteUrl}${image.src}` - content = content.replace( - new RegExp( - `!\\[.*?\\]\\(${originalSrc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, - 'g' - ), - `![${fileName}](${fullImageUrl})` - ) - } catch (error) { - console.warn(`Failed to process image: ${originalSrc}`, error) - } - } - } - } - - // Convert Markdown to HTML - const htmlContent = md.render(content) - - return { - title: post.data.title, - link: `/${post.id}/`, - pubDate: post.data.pubDate, - content: htmlContent - } - }) - ) - - return rss({ - title: themeConfig.site.title, - description: themeConfig.site.description, - site: context.site || themeConfig.site.website, - items, - customData: `en-US` - }) -} +export const GET = (context: APIContext) => generateRSS(context) diff --git a/src/utils/feed.ts b/src/utils/feed.ts index 27236b5..2f2e2a3 100644 --- a/src/utils/feed.ts +++ b/src/utils/feed.ts @@ -1,256 +1,191 @@ +import type { APIContext, ImageMetadata } from 'astro' +import { getImage } from 'astro:assets' import { getCollection, type CollectionEntry } from 'astro:content' -import { themeConfig } from '@/config' -import type { APIContext } from 'astro' +import { Feed } from 'feed' import MarkdownIt from 'markdown-it' -import { parse } from 'node-html-parser' +import { parse as htmlParser } from 'node-html-parser' import sanitizeHtml from 'sanitize-html' +import { themeConfig } from '@/config' +import path from 'node:path' -// Dynamically import all post images -const imageModules = import.meta.glob('/src/content/posts/_assets/*.(jpg|jpeg|png|gif|webp)', { - eager: true, - query: '?url', - import: 'default' -}) - -// Create image path mapping: filename -> URL -const imagePathMap = new Map() -for (const [path, url] of Object.entries(imageModules)) { - // Extract filename from path - const fileName = path.split('/').pop() || '' - imagePathMap.set(fileName, url as string) -} - -// Create markdown-it instance with Astro-like configuration -const md = new MarkdownIt({ +const markdownParser = new MarkdownIt({ html: true, linkify: true, typographer: true }) -// Custom image renderer to preserve original paths -md.renderer.rules.image = function (tokens, idx) { - const token = tokens[idx] - if (!token.attrs) return '' - - const srcIndex = token.attrIndex('src') - const src = srcIndex >= 0 ? token.attrs[srcIndex][1] : '' - const altIndex = token.attrIndex('alt') - const alt = altIndex >= 0 ? token.attrs[altIndex][1] : '' +const imagesGlob = import.meta.glob<{ default: ImageMetadata }>( + '/src/content/posts/_assets/**/*.{jpeg,jpg,png,gif,webp}' +) + +/** + * Fix relative image paths in HTML content and convert them to absolute URLs + * @param htmlContent - HTML string converted from Markdown + * @param baseUrl - Base URL of the website + * @param postPath - Current post path (e.g., 'some-post.md' or 'tech/another-post.md') + * @returns - HTML string with processed image paths + */ +async function fixRelativeImagePaths( + htmlContent: string, + baseUrl: string, + postPath: string +): Promise { + const root = htmlParser(htmlContent) + const imageTags = root.querySelectorAll('img') + const postDir = path.dirname(postPath) + + for (const img of imageTags) { + const src = img.getAttribute('src') + if (!src) continue - return `${alt}` -} + if (/^(https?:\/\/|\/\/)/.test(src)) { + continue + } -// Process image paths, convert relative paths to absolute paths -function processImagePaths(html: string, siteUrl: string): string { - const root = parse(html) + if (src.startsWith('./') || src.startsWith('../')) { + // Build path relative to /src/content/posts + let resolvedPath: string + if (src.startsWith('./')) { + // ./xxx -> postDir/xxx + resolvedPath = path.posix.join('/src/content/posts', postDir, src.slice(2)) + } else { + // ../xxx -> Resolve to parent directory + resolvedPath = path.posix.resolve('/src/content/posts', postDir, src) + } - // Process img tags - root.querySelectorAll('img').forEach((img) => { - const src = img.getAttribute('src') - if (src && !src.startsWith('http') && !src.startsWith('//')) { - // If it's a relative path, convert to absolute path - let absoluteSrc: string - if (src.startsWith('/')) { - // Absolute path (relative to website root) - absoluteSrc = `${siteUrl}${src}` - } else if (src.startsWith('./_assets/')) { - // Relative path (starting with ./_assets/) - use image mapping - const fileName = src.substring(2) // Remove ./ - const mappedUrl = imagePathMap.get(fileName.split('/').pop() || '') - if (mappedUrl) { - // Ensure mapped URL is absolute - absoluteSrc = mappedUrl.startsWith('http') ? mappedUrl : `${siteUrl}${mappedUrl}` - } else { - // If mapping not found, use original path - absoluteSrc = `${siteUrl}/${fileName}` - } - } else if (src.startsWith('_assets/')) { - // Relative path (starting with _assets/) - use image mapping - const fileName = src.split('/').pop() || '' - const mappedUrl = imagePathMap.get(fileName) - if (mappedUrl) { - // Ensure mapped URL is absolute - absoluteSrc = mappedUrl.startsWith('http') ? mappedUrl : `${siteUrl}${mappedUrl}` - } else { - // If mapping not found, use original path - absoluteSrc = `${siteUrl}/${src}` + // Check if corresponding image module exists + if (imagesGlob[resolvedPath]) { + try { + const imageModule = await imagesGlob[resolvedPath]() + const metadata = imageModule.default + + // In development environment, don't process images, use original paths to ensure cross-platform compatibility + if (import.meta.env.DEV) { + // Development environment: use relative paths + const relativePath = resolvedPath.replace('/src/content/posts/', '/') + const imageUrl = new URL(relativePath, baseUrl).toString() + img.setAttribute('src', imageUrl) + } else { + // Production environment: use getImage optimization + const processedImage = await getImage({ + src: metadata, + format: 'webp', + width: 800 + }) + + // Always use the optimized image path in production + img.setAttribute('src', new URL(processedImage.src, baseUrl).toString()) + } + } catch (error) { + console.error(`[Feed] Image processing failed: ${src} -> ${resolvedPath}`, error) + // Use original path as fallback when error occurs + const relativePath = resolvedPath.replace('/src/content/posts/', '/') + const imageUrl = new URL(relativePath, baseUrl).toString() + img.setAttribute('src', imageUrl) } - } else if (src.startsWith('./')) { - // Other relative paths (starting with ./) - const fileName = src.substring(2) // Remove ./ - absoluteSrc = `${siteUrl}/_assets/${fileName}` } else { - // Other relative paths - absoluteSrc = `${siteUrl}/_assets/${src}` + console.warn(`[Feed] Image module not found: ${resolvedPath}`) + console.warn(`[Feed] Available image modules:`, Object.keys(imagesGlob)) } - img.setAttribute('src', absoluteSrc) + } else if (src.startsWith('/')) { + img.setAttribute('src', new URL(src, baseUrl).toString()) } - }) + } return root.toString() } -// Clean and format HTML -function sanitizeAndFormat(html: string): string { - return sanitizeHtml(html, { - allowedTags: [ - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'p', - 'br', - 'hr', - 'ul', - 'ol', - 'li', - 'blockquote', - 'pre', - 'code', - 'strong', - 'em', - 'del', - 'mark', - 'a', - 'img', - 'table', - 'thead', - 'tbody', - 'tr', - 'th', - 'td', - 'sub', - 'sup', - 'abbr', - 'kbd', - 'div', - 'span' - ], - allowedAttributes: { - '*': ['class', 'id'], - a: ['href', 'title', 'target', 'rel'], - img: ['src', 'alt', 'title', 'width', 'height'], - abbr: ['title'], - code: ['class'], - pre: ['class'] +/** + * Generate a generic Feed instance + */ +async function generateFeedInstance(context: APIContext) { + const siteUrl = (context.site?.toString() || themeConfig.site.website).replace(/\/$/, '') + const { title = '', description = '', author = '', language = 'en-US' } = themeConfig.site + + const feed = new Feed({ + title: title, + description: description, + id: siteUrl, + link: siteUrl, + language: language, + copyright: `Copyright © ${new Date().getFullYear()} ${author}`, + updated: new Date(), + generator: 'Astro Chiri Feed Generator', + feedLinks: { + rss: `${siteUrl}/rss.xml`, + atom: `${siteUrl}/atom.xml` }, - allowedSchemes: ['http', 'https', 'mailto', 'tel'], - transformTags: { - a: (tagName: string, attribs: Record) => { - // Ensure external links open in a new window - if (attribs.href && attribs.href.startsWith('http')) { - return { - tagName, - attribs: { - ...attribs, - target: '_blank', - rel: 'noopener noreferrer' - } - } - } - return { tagName, attribs } - } + author: { + name: author, + link: siteUrl } }) -} -export async function generateRSS(context: APIContext) { - const posts = await getCollection('posts') - const filteredPosts = posts.filter((post: CollectionEntry<'posts'>) => !post.id.startsWith('_')) - const sortedPosts = filteredPosts.sort( + const posts = await getCollection( + 'posts', + ({ id }: CollectionEntry<'posts'>) => !id.startsWith('_') + ) + const sortedPosts = posts.sort( (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() ) - // Ensure site URL doesn't end with slash - const siteUrl = (context.site?.toString() || themeConfig.site.website).replace(/\/$/, '') - - const rss = ` - - - ${themeConfig.site.title} - ${siteUrl} - ${themeConfig.site.description} - zh-CN - ${new Date().toUTCString()} - - ${sortedPosts - .map((post: CollectionEntry<'posts'>) => { - // Convert Markdown to HTML - const rawHtml = md.render(post.body || '') + for (const post of sortedPosts) { + const postSlug = post.id.replace(/\.[^/.]+$/, '') + const postUrl = new URL(postSlug, siteUrl).toString() + const rawHtml = markdownParser.render(post.body || '') + const processedHtml = await fixRelativeImagePaths(rawHtml, siteUrl, post.id) + const cleanHtml = sanitizeHtml(processedHtml, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'div', 'span']), + allowedAttributes: { + ...sanitizeHtml.defaults.allowedAttributes, + '*': ['class', 'id'], + a: ['href', 'title', 'target', 'rel'], + img: ['src', 'alt', 'title', 'width', 'height'] + } + }) - // Process image paths - const processedHtml = processImagePaths(rawHtml, siteUrl) - // Clean and format HTML - const cleanHtml = sanitizeAndFormat(processedHtml) + feed.addItem({ + title: post.data.title, + id: postUrl, + link: postUrl, + content: cleanHtml, + date: post.data.pubDate, + published: post.data.pubDate + }) + } - return ` - - <![CDATA[${post.data.title}]]> - ${siteUrl}/${post.id}/ - ${siteUrl}/${post.id}/ - ${post.data.pubDate.toUTCString()} - - - ` - }) - .join('')} - -` + return feed +} - return new Response(rss, { - headers: { - 'Content-Type': 'application/xml; charset=utf-8' - } +/** + * Generate RSS 2.0 feed + */ +export async function generateRSS(context: APIContext) { + const feed = await generateFeedInstance(context) + const rssXml = feed + .rss2() + .replace( + '', + '\n' + ) + return new Response(rssXml, { + headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } }) } +/** + * Generate Atom 1.0 feed + */ export async function generateAtom(context: APIContext) { - const posts = await getCollection('posts') - const filteredPosts = posts.filter((post: CollectionEntry<'posts'>) => !post.id.startsWith('_')) - const sortedPosts = filteredPosts.sort( - (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => - b.data.pubDate.valueOf() - a.data.pubDate.valueOf() - ) - - // Ensure site URL doesn't end with slash - const siteUrl = (context.site?.toString() || themeConfig.site.website).replace(/\/$/, '') - - const atom = ` - - ${themeConfig.site.title} - ${themeConfig.site.description} - - - ${siteUrl} - ${new Date().toISOString()} - ${sortedPosts - .map((post: CollectionEntry<'posts'>) => { - // Convert Markdown to HTML - const rawHtml = md.render(post.body || '') - // Process image paths - const processedHtml = processImagePaths(rawHtml, siteUrl) - // Clean and format HTML - const cleanHtml = sanitizeAndFormat(processedHtml) - - return ` - - ${post.data.title} - - ${siteUrl}/${post.id}/ - ${post.data.pubDate.toISOString()} - - - ` - }) - .join('')} -` - - return new Response(atom, { - headers: { - 'Content-Type': 'application/xml; charset=utf-8' - } + const feed = await generateFeedInstance(context) + const atomXml = feed + .atom1() + .replace( + '', + '\n' + ) + return new Response(atomXml, { + headers: { 'Content-Type': 'application/atom+xml; charset=utf-8' } }) }