feat: rss feed

closes #2
This commit is contained in:
CDN 2025-02-03 16:31:04 +08:00
parent e30d41db2f
commit 4ba16a7ddd
Signed by: CDN
GPG key ID: 0C656827F9F80080
4 changed files with 147 additions and 4 deletions

View file

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vite build && tsx scripts/generate-rss.ts",
"lint": "eslint .",
"preview": "vite preview"
},
@ -14,6 +14,7 @@
"@tanstack/react-query": "^5.66.0",
"antd": "^5.23.3",
"clsx": "^2.1.1",
"feed": "^4.2.2",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^7.2.2",
"lucide-react": "^0.344.0",

24
pnpm-lock.yaml generated
View file

@ -20,6 +20,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
feed:
specifier: ^4.2.2
version: 4.2.2
i18next:
specifier: ^23.16.8
version: 23.16.8
@ -1138,6 +1141,10 @@ packages:
fastq@1.18.0:
resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==}
feed@4.2.2:
resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==}
engines: {node: '>=0.4.0'}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -1845,6 +1852,9 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@ -2039,6 +2049,10 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -3103,6 +3117,10 @@ snapshots:
dependencies:
reusify: 1.0.4
feed@4.2.2:
dependencies:
xml-js: 1.6.11
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -3857,6 +3875,8 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
sax@1.4.1: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@ -4038,6 +4058,10 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
xml-js@1.6.11:
dependencies:
sax: 1.4.1
yallist@3.1.1: {}
yaml@2.7.0: {}

103
scripts/generate-rss.ts Normal file
View file

@ -0,0 +1,103 @@
import { Feed } from 'feed';
import fs from 'fs-extra';
import path from 'path';
import { Update } from '../src/utils/updates';
const SITE_URL = 'https://starset.wiki';
interface LanguageConfig {
code: string;
dataDir: string;
title: string;
description: string;
}
const LANGUAGES: LanguageConfig[] = [
{
code: 'en-US',
dataDir: 'en-US',
title: 'STARSET Mirror Site Updates',
description: 'Latest updates from STARSET Mirror Site'
},
{
code: 'zh-CN',
dataDir: 'zh-CN',
title: 'STARSET 镜像站更新',
description: 'STARSET 镜像站的最新更新'
},
{
code: 'zh-Hant',
dataDir: 'zh-TW',
title: 'STARSET 鏡像站更新',
description: 'STARSET 鏡像站的最新更新'
}
];
async function generateRSSFeed(lang: LanguageConfig) {
// Read all updates
const updatesIndex = await fs.readJson(path.join('data', lang.dataDir, 'updates.json'));
const years = updatesIndex.years as string[];
let allUpdates: Update[] = [];
for (const year of years) {
const yearData = await fs.readJson(path.join('data', lang.dataDir, 'updates', year));
allUpdates = [...allUpdates, ...yearData.updates];
}
// Sort updates by date in descending order
allUpdates.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
// Take the latest 10 updates
const latestUpdates = allUpdates.slice(0, 10);
// Create feed
const feed = new Feed({
title: lang.title,
description: lang.description,
id: `${SITE_URL}/${lang.code}/updates`,
link: `${SITE_URL}/${lang.code}/updates`,
language: lang.code,
favicon: `${SITE_URL}/favicon.ico`,
copyright: "All rights reserved",
updated: latestUpdates[0] ? new Date(latestUpdates[0].date) : new Date(),
feedLinks: {
rss2: `${SITE_URL}/${lang.code}/rss.xml`
}
});
// Add items to feed
for (const update of latestUpdates) {
feed.addItem({
title: update.title,
id: update.id,
link: update.link || `${SITE_URL}/${lang.code}/updates#${update.id}`,
description: update.summary,
date: new Date(update.date),
category: update.tags.map(tag => ({ name: tag }))
});
}
// Create dist directory if it doesn't exist
const distPath = path.join('dist', lang.code);
await fs.ensureDir(distPath);
// Write feed to file
await fs.writeFile(
path.join(distPath, 'rss.xml'),
feed.rss2()
);
}
async function main() {
try {
for (const lang of LANGUAGES) {
await generateRSSFeed(lang);
console.log(`Generated RSS feed for ${lang.code}`);
}
} catch (error) {
console.error('Error generating RSS feeds:', error);
process.exit(1);
}
}
main();

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useSearchParams } from 'react-router-dom';
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react';
import { RssIcon } from 'lucide-react'; // Add this line
import { Update, useUpdates, getUpdateUrl } from '../utils/updates';
import TagFilter from './ui/TagFilter';
@ -60,6 +61,9 @@ const Pagination: React.FC<PaginationProps> = ({
const Timeline = () => {
const { t, i18n } = useTranslation();
const LANGUAGE_CODE_MAP: Record<string, string> = {
'zh-TW': 'zh-Hant'
};
const [searchParams, setSearchParams] = useSearchParams();
// 获取当前页码和标签筛选
@ -118,9 +122,20 @@ const Timeline = () => {
<div className="container mx-auto px-8 md:px-20 lg:px-32 xl:px-48 2xl:px-64">
<div className="max-w-5xl">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-12 pl-16">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4 md:mb-0">
<div className="flex items-center gap-2">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">
{t('updates.title')}
</h2>
<a
href={`/${LANGUAGE_CODE_MAP[i18n.language] || i18n.language}/rss.xml`}
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
title="RSS Feed"
>
<RssIcon size={20} />
</a>
</div>
<TagFilter
availableTags={availableTags}