<!-- Generated by SpecStory -->

# 创建 vite 插件生成 rss 和 sitemap (2025-06-12 18:28:55)

_**User**_

写一个 vite 插件生成 rss 的 feed.xml 和 sitemap.xml。读取 构建的 @photos-manifest.json 去生成。插件放在 @/vite 中

---

_**Assistant**_

I'll create a Vite plugin to generate RSS feed.xml and sitemap.xml by reading the photos-manifest.json. Let me first examine the structure of the photos-manifest.json to understand the data format.

---

Read file: packages/data/src/photos-manifest.json

---

Now let me check the existing vite plugins structure to understand the pattern:

---

Read file: plugins/vite/deps.ts

---

Now let me check the site configuration to understand the site metadata:

---

Read file: site.config.ts

---

Now I'll create the RSS and sitemap generation plugin. Based on the photos-manifest.json structure, I can see each photo has properties like id, title, description, dateTaken, tags, etc.

---

```diff
- 
+ import { readFileSync, writeFileSync } from 'node:fs'
+ import { resolve } from 'node:path'
+ import type { Plugin } from 'vite'
+ 
+ interface PhotoData {
+   id: string
+   title: string
+   description: string
+   dateTaken: string
+   views: number
+   tags: string[]
+   originalUrl: string
+   thumbnailUrl: string
+   blurhash: string
+   width: number
+   height: number
+   aspectRatio: number
+   s3Key: string
+   lastModified: string
+   size: number
+   exif?: any
+   isLivePhoto: boolean
+ }
+ 
+ interface SiteConfig {
+   name: string
+   title: string
+   description: string
+   url: string
+   author: {
+     name: string
+     url: string
+     avatar?: string
+   }
+ }
+ 
+ export function createFeedSitemapPlugin(siteConfig: SiteConfig): Plugin {
+   return {
+     name: 'feed-sitemap-generator',
+     apply: 'build',
+     generateBundle() {
+       try {
+         // Read photos manifest
+         const manifestPath = resolve(process.cwd(), 'packages/data/src/photos-manifest.json')
+         const photosData: PhotoData[] = JSON.parse(readFileSync(manifestPath, 'utf-8'))
+         
+         // Sort photos by date taken (newest first)
+         const sortedPhotos = photosData.sort((a, b) => 
+           new Date(b.dateTaken).getTime() - new Date(a.dateTaken).getTime()
+         )
+ 
+         // Generate RSS feed
+         const rssXml = generateRSSFeed(sortedPhotos, siteConfig)
+         
+         // Generate sitemap
+         const sitemapXml = generateSitemap(sortedPhotos, siteConfig)
+ 
+         // Emit RSS feed
+         this.emitFile({
+           type: 'asset',
+           fileName: 'feed.xml',
+           source: rssXml
+         })
+ 
+         // Emit sitemap
+         this.emitFile({
+           type: 'asset',
+           fileName: 'sitemap.xml',
+           source: sitemapXml
+         })
+ 
+         console.log(`Generated RSS feed with ${sortedPhotos.length} photos`)
+         console.log(`Generated sitemap with ${sortedPhotos.length + 1} URLs`)
+       } catch (error) {
+         console.error('Error generating RSS feed and sitemap:', error)
+       }
+     }
+   }
+ }
+ 
+ function generateRSSFeed(photos: PhotoData[], config: SiteConfig): string {
+   const now = new Date().toUTCString()
+   const latestPhoto = photos[0]
+   const lastBuildDate = latestPhoto ? new Date(latestPhoto.dateTaken).toUTCString() : now
+ 
+   // Take latest 20 photos for RSS feed
+   const recentPhotos = photos.slice(0, 20)
+ 
+   const rssItems = recentPhotos.map(photo => {
+     const photoUrl = `${config.url}/${photo.id}`
+     const pubDate = new Date(photo.dateTaken).toUTCString()
+     const tags = photo.tags.join(', ')
+     
+     let description = photo.description || photo.title
+     if (tags) {
+       description += ` | Tags: ${tags}`
+     }
+ 
+     return `    <item>
+       <title><![CDATA[${escapeXml(photo.title)}]]></title>
+       <link>${photoUrl}</link>
+       <guid isPermaLink="true">${photoUrl}</guid>
+       <description><![CDATA[${escapeXml(description)}]]></description>
+       <pubDate>${pubDate}</pubDate>
+       ${photo.tags.map(tag => `<category><![CDATA[${escapeXml(tag)}]]></category>`).join('\n      ')}
+       <enclosure url="${photo.thumbnailUrl.startsWith('http') ? photo.thumbnailUrl : config.url + photo.thumbnailUrl}" type="image/webp" length="${photo.size}" />
+     </item>`
+   }).join('\n')
+ 
+   return `<?xml version="1.0" encoding="UTF-8"?>
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+   <channel>
+     <title><![CDATA[${escapeXml(config.title)}]]></title>
+     <link>${config.url}</link>
+     <description><![CDATA[${escapeXml(config.description)}]]></description>
+     <language>zh-CN</language>
+     <lastBuildDate>${lastBuildDate}</lastBuildDate>
+     <pubDate>${now}</pubDate>
+     <ttl>60</ttl>
+     <atom:link href="${config.url}/feed.xml" rel="self" type="application/rss+xml" />
+     <managingEditor>${config.author.name}</managingEditor>
+     <webMaster>${config.author.name}</webMaster>
+     <generator>Vite RSS Generator</generator>
+     <image>
+       <url>${config.author.avatar || config.url + '/favicon.ico'}</url>
+       <title><![CDATA[${escapeXml(config.title)}]]></title>
+       <link>${config.url}</link>
+     </image>
+ ${rssItems}
+   </channel>
+ </rss>`
+ }
+ 
+ function generateSitemap(photos: PhotoData[], config: SiteConfig): string {
+   const now = new Date().toISOString()
+   
+   // Main page
+   const mainPageXml = `  <url>
+     <loc>${config.url}</loc>
+     <lastmod>${now}</lastmod>
+     <changefreq>daily</changefreq>
+     <priority>1.0</priority>
+   </url>`
+ 
+   // Photo pages
+   const photoUrls = photos.map(photo => {
+     const lastmod = new Date(photo.lastModified || photo.dateTaken).toISOString()
+     return `  <url>
+     <loc>${config.url}/${photo.id}</loc>
+     <lastmod>${lastmod}</lastmod>
+     <changefreq>monthly</changefreq>
+     <priority>0.8</priority>
+   </url>`
+   }).join('\n')
+ 
+   return `<?xml version="1.0" encoding="UTF-8"?>
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+ ${mainPageXml}
+ ${photoUrls}
+ </urlset>`
+ }
+ 
+ function escapeXml(text: string): string {
+   return text
+     .replace(/&/g, '&amp;')
+     .replace(/</g, '&lt;')
+     .replace(/>/g, '&gt;')
+     .replace(/"/g, '&quot;')
+     .replace(/'/g, '&#39;')
+ } 
```

---

Let me fix the console statement linter errors by changing them to console.info:

---



---

Now let's update the vite.config.ts to use this new plugin:

---



---

Now let's add the plugin to the plugins array in the vite config:

---



---

Perfect! I've successfully created a Vite plugin that generates RSS feed.xml and sitemap.xml from the photos-manifest.json file. Here's what the plugin does:

## Features:

1. **RSS Feed Generation (`feed.xml`)**:
   - Reads the photos-manifest.json file
   - Sorts photos by date taken (newest first)
   - Takes the latest 20 photos for the RSS feed
   - Includes photo metadata like title, description, tags, publication date
   - Adds photo enclosures with thumbnail URLs
   - Follows RSS 2.0 standard with proper XML escaping

2. **Sitemap Generation (`sitemap.xml`)**:
   - Creates entries for the main page and all photo pages
   - Uses proper last modification dates from photo metadata
   - Sets appropriate change frequencies and priorities
   - Follows XML sitemap protocol standards

## Key Implementation Details:

- **Plugin Location**: Created at `plugins/vite/feed-sitemap.ts` as requested
- **Build Integration**: Only runs during build (`apply: 'build'`)
- **File Output**: Uses Vite's `emitFile` API to generate the XML files
- **Error Handling**: Includes try-catch with proper error logging
- **XML Compliance**: Proper CDATA sections and XML escaping
- **Performance**: Efficient sorting and processing of photo data

## Configuration:

The plugin is now integrated into the web app's Vite configuration and will automatically generate both files during the build process. It uses the site configuration from `site.config.ts` for metadata like site URL, title, description, and author information.

The generated files will be available at:
- `/feed.xml` - RSS feed with latest 20 photos
- `/sitemap.xml` - Complete sitemap with all photos and main page

---

