Skip to content

Commit 89dea81

Browse files
committed
Reimplement RSS plugin
1 parent 2e165dc commit 89dea81

4 files changed

Lines changed: 286 additions & 777 deletions

File tree

.vitepress/config.mts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,8 @@
11
import { defineConfig, HeadConfig } from 'vitepress'
2-
import { RssPlugin } from 'vitepress-plugin-rss'
2+
import { generateRss } from './rss'
33

44
const baseUrl = 'https://php-testo.github.io'
55

6-
const rssOptionsEn = {
7-
title: 'Testo Blog',
8-
baseUrl,
9-
copyright: 'Copyright © Testo',
10-
description: 'Updates from Testo - Modern PHP Testing Framework',
11-
filename: 'feed.xml',
12-
filter: (post: { url: string }) => post.url.startsWith('/blog/') && !post.url.endsWith('/blog/'),
13-
icon: false,
14-
}
15-
16-
const rssOptionsRu = {
17-
title: 'Блог Testo',
18-
baseUrl,
19-
copyright: 'Copyright © Testo',
20-
description: 'Новости Testo - современного PHP фреймворка для тестирования',
21-
filename: 'ru/feed.xml',
22-
filter: (post: { url: string }) => post.url.startsWith('/ru/blog/') && !post.url.endsWith('/blog/'),
23-
icon: false,
24-
}
25-
266
export default defineConfig({
277
title: 'Testo',
288
description: 'Modern PHP Testing Framework',
@@ -36,9 +16,7 @@ export default defineConfig({
3616
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
3717
],
3818

39-
vite: {
40-
plugins: [RssPlugin(rssOptionsEn), RssPlugin(rssOptionsRu)],
41-
},
19+
buildEnd: generateRss,
4220

4321
locales: {
4422
root: {

.vitepress/rss.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { writeFileSync, mkdirSync } from 'fs'
2+
import path from 'path'
3+
import { Feed } from 'feed'
4+
import { createContentLoader, SiteConfig } from 'vitepress'
5+
6+
const baseUrl = 'https://php-testo.github.io'
7+
8+
interface FeedConfig {
9+
title: string
10+
description: string
11+
filename: string
12+
filter: (url: string) => boolean
13+
lang: string
14+
}
15+
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+
]
32+
33+
export async function generateRss(config: SiteConfig) {
34+
const posts = await createContentLoader(['blog/*.md', 'ru/blog/*.md'], {
35+
excerpt: false,
36+
render: false,
37+
}).load()
38+
39+
for (const feedConfig of feeds) {
40+
const feed = new Feed({
41+
title: feedConfig.title,
42+
description: feedConfig.description,
43+
id: baseUrl,
44+
link: baseUrl,
45+
language: feedConfig.lang,
46+
copyright: `Copyright © ${new Date().getFullYear()} Testo`,
47+
generator: 'VitePress + feed',
48+
feedLinks: {
49+
rss: `${baseUrl}/${feedConfig.filename}`,
50+
},
51+
})
52+
53+
const filteredPosts = posts
54+
.filter((post) => feedConfig.filter(post.url))
55+
.sort((a, b) => {
56+
const dateA = new Date(a.frontmatter.date || 0).getTime()
57+
const dateB = new Date(b.frontmatter.date || 0).getTime()
58+
return dateB - dateA
59+
})
60+
61+
for (const post of filteredPosts) {
62+
const url = `${baseUrl}${post.url}`
63+
const imageUrl = post.frontmatter.image ? `${baseUrl}${post.frontmatter.image}` : undefined
64+
65+
feed.addItem({
66+
title: post.frontmatter.title || 'Untitled',
67+
id: url,
68+
link: url,
69+
description: post.frontmatter.description || '',
70+
date: new Date(post.frontmatter.date || Date.now()),
71+
author: post.frontmatter.author
72+
? [{ name: post.frontmatter.author }]
73+
: [{ name: 'Testo Team' }],
74+
image: imageUrl,
75+
})
76+
}
77+
78+
const outDir = config.outDir
79+
const filePath = path.join(outDir, feedConfig.filename)
80+
81+
// Ensure directory exists
82+
mkdirSync(path.dirname(filePath), { recursive: true })
83+
84+
// Generate RSS
85+
let rssContent = feed.rss2()
86+
87+
// Add dc namespace
88+
if (!rssContent.includes('xmlns:dc=')) {
89+
rssContent = rssContent.replace(
90+
'<rss version="2.0"',
91+
'<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"'
92+
)
93+
}
94+
95+
// Add dc:creator for each post
96+
for (const post of filteredPosts) {
97+
const author = post.frontmatter.author || 'Testo Team'
98+
const url = `${baseUrl}${post.url}`
99+
// Insert dc:creator after </description> for this item
100+
rssContent = rssContent.replace(
101+
new RegExp(`(<guid isPermaLink="false">${url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}</guid>)`),
102+
`$1\n <dc:creator><![CDATA[${author}]]></dc:creator>`
103+
)
104+
}
105+
106+
writeFileSync(filePath, rssContent)
107+
console.log(`✓ RSS generated: ${feedConfig.filename} (${filteredPosts.length} posts)`)
108+
}
109+
}

0 commit comments

Comments
 (0)