1- import { writeFileSync , mkdirSync } from 'fs'
1+ import { writeFileSync , mkdirSync , readdirSync , readFileSync } from 'fs'
22import path from 'path'
33import { Feed } from 'feed'
44import { createContentLoader , SiteConfig } from 'vitepress'
5+ import type { Plugin } from 'vite'
6+ // @ts -ignore
7+ import matter from 'gray-matter'
8+ import { locales , getBlogFolder , getBlogUrl , getFeedFilename , getBlogGlobPatterns } from './locales'
59
610const baseUrl = 'https://php-testo.github.io'
711
812interface FeedConfig {
913 title : string
1014 description : string
1115 filename : string
12- filter : ( url : string ) => boolean
16+ blogFolder : string
17+ blogUrl : string
1318 lang : string
1419}
1520
16- const feeds : FeedConfig [ ] = [
17- {
18- title : 'Testo Blog' ,
19- description : 'Updates from Testo - Modern PHP Testing Framework' ,
20- filename : 'feed.xml' ,
21- filter : ( url ) => url . startsWith ( '/blog/' ) && url !== '/blog/' ,
22- lang : 'en' ,
23- } ,
24- {
25- title : 'Блог Testo' ,
26- description : 'Новости Testo - современного PHP фреймворка для тестирования' ,
27- filename : 'ru/feed.xml' ,
28- filter : ( url ) => url . startsWith ( '/ru/blog/' ) && url !== '/ru/blog/' ,
29- lang : 'ru' ,
30- } ,
31- ]
21+ // Generate feed configs from locales
22+ const feeds : FeedConfig [ ] = locales . map ( locale => ( {
23+ title : locale . blogTitle ,
24+ description : locale . blogDescription ,
25+ filename : getFeedFilename ( locale ) ,
26+ blogFolder : getBlogFolder ( locale ) ,
27+ blogUrl : getBlogUrl ( locale ) ,
28+ lang : locale . code ,
29+ } ) )
3230
3331export async function generateRss ( config : SiteConfig ) {
34- const posts = await createContentLoader ( [ 'blog/*.md' , 'ru/blog/*.md' ] , {
32+ const posts = await createContentLoader ( getBlogGlobPatterns ( ) , {
3533 excerpt : false ,
3634 render : false ,
3735 } ) . load ( )
@@ -51,7 +49,7 @@ export async function generateRss(config: SiteConfig) {
5149 } )
5250
5351 const filteredPosts = posts
54- . filter ( ( post ) => feedConfig . filter ( post . url ) )
52+ . filter ( ( post ) => post . url . startsWith ( feedConfig . blogUrl ) && post . url !== feedConfig . blogUrl )
5553 . sort ( ( a , b ) => {
5654 const dateA = new Date ( a . frontmatter . date || 0 ) . getTime ( )
5755 const dateB = new Date ( b . frontmatter . date || 0 ) . getTime ( )
@@ -107,3 +105,120 @@ export async function generateRss(config: SiteConfig) {
107105 console . log ( `✓ RSS generated: ${ feedConfig . filename } (${ filteredPosts . length } posts)` )
108106 }
109107}
108+
109+ // Generate RSS content for dev server (without createContentLoader)
110+ function generateRssContent ( feedConfig : FeedConfig , docsRoot : string ) : string {
111+ const feed = new Feed ( {
112+ title : feedConfig . title ,
113+ description : feedConfig . description ,
114+ id : baseUrl ,
115+ link : baseUrl ,
116+ language : feedConfig . lang ,
117+ copyright : `Copyright © ${ new Date ( ) . getFullYear ( ) } Testo` ,
118+ generator : 'VitePress + feed' ,
119+ feedLinks : {
120+ rss : `${ baseUrl } /${ feedConfig . filename } ` ,
121+ } ,
122+ } )
123+
124+ const blogPath = path . join ( docsRoot , feedConfig . blogFolder )
125+
126+ // Read markdown files directly
127+ const posts : Array < {
128+ url : string
129+ frontmatter : Record < string , any >
130+ } > = [ ]
131+
132+ try {
133+ const files = readdirSync ( blogPath ) . filter ( ( f : string ) => f . endsWith ( '.md' ) && f !== 'index.md' )
134+
135+ for ( const file of files ) {
136+ const filePath = path . join ( blogPath , file )
137+ const content = readFileSync ( filePath , 'utf-8' )
138+ const { data : frontmatter } = matter ( content )
139+ const slug = file . replace ( / \. m d $ / , '' )
140+ const url = `${ feedConfig . blogUrl } ${ slug } `
141+
142+ posts . push ( { url, frontmatter } )
143+ }
144+ } catch ( e ) {
145+ // Blog folder might not exist
146+ }
147+
148+ // Sort by date
149+ posts . sort ( ( a , b ) => {
150+ const dateA = new Date ( a . frontmatter . date || 0 ) . getTime ( )
151+ const dateB = new Date ( b . frontmatter . date || 0 ) . getTime ( )
152+ return dateB - dateA
153+ } )
154+
155+ for ( const post of posts ) {
156+ const url = `${ baseUrl } ${ post . url } `
157+ const imageUrl = post . frontmatter . image ? `${ baseUrl } ${ post . frontmatter . image } ` : undefined
158+
159+ feed . addItem ( {
160+ title : post . frontmatter . title || 'Untitled' ,
161+ id : url ,
162+ link : url ,
163+ description : post . frontmatter . description || '' ,
164+ date : new Date ( post . frontmatter . date || Date . now ( ) ) ,
165+ author : post . frontmatter . author
166+ ? [ { name : post . frontmatter . author } ]
167+ : [ { name : 'Testo Team' } ] ,
168+ image : imageUrl ,
169+ } )
170+ }
171+
172+ // Generate RSS
173+ let rssContent = feed . rss2 ( )
174+
175+ // Add dc namespace
176+ if ( ! rssContent . includes ( 'xmlns:dc=' ) ) {
177+ rssContent = rssContent . replace (
178+ '<rss version="2.0"' ,
179+ '<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"'
180+ )
181+ }
182+
183+ // Add dc:creator for each post
184+ for ( const post of posts ) {
185+ const author = post . frontmatter . author || 'Testo Team'
186+ const url = `${ baseUrl } ${ post . url } `
187+ rssContent = rssContent . replace (
188+ new RegExp ( `(<guid isPermaLink="false">${ url . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } </guid>)` ) ,
189+ `$1\n <dc:creator><![CDATA[${ author } ]]></dc:creator>`
190+ )
191+ }
192+
193+ return rssContent
194+ }
195+
196+ // Vite plugin for dev server RSS
197+ export function rssPlugin ( ) : Plugin {
198+ let docsRoot : string
199+
200+ return {
201+ name : 'vitepress-rss-dev' ,
202+ configResolved ( config ) {
203+ // Get docs root from VitePress config
204+ docsRoot = config . root
205+ } ,
206+ configureServer ( server ) {
207+ server . middlewares . use ( ( req , res , next ) => {
208+ const url = req . url || ''
209+
210+ // Check if requesting RSS feed
211+ const feedConfig = feeds . find ( f => url === `/${ f . filename } ` )
212+
213+ if ( feedConfig ) {
214+ const rssContent = generateRssContent ( feedConfig , docsRoot )
215+ res . setHeader ( 'Content-Type' , 'application/rss+xml; charset=utf-8' )
216+ res . end ( rssContent )
217+ return
218+ }
219+
220+ next ( )
221+ } )
222+ } ,
223+ }
224+ }
0 commit comments