From 4ba16a7dddb1dbd9239acb4f6cd30f3c6a9c93cf Mon Sep 17 00:00:00 2001 From: cdn0x12 Date: Mon, 3 Feb 2025 16:31:04 +0800 Subject: [PATCH] feat: rss feed closes #2 --- package.json | 3 +- pnpm-lock.yaml | 24 +++++++++ scripts/generate-rss.ts | 103 ++++++++++++++++++++++++++++++++++++ src/components/Timeline.tsx | 21 ++++++-- 4 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 scripts/generate-rss.ts diff --git a/package.json b/package.json index 9645731..1c30b6c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16e3736..a00a9f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/scripts/generate-rss.ts b/scripts/generate-rss.ts new file mode 100644 index 0000000..449fb54 --- /dev/null +++ b/scripts/generate-rss.ts @@ -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(); diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index e9157e1..121a531 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -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 = ({ const Timeline = () => { const { t, i18n } = useTranslation(); + const LANGUAGE_CODE_MAP: Record = { + 'zh-TW': 'zh-Hant' + }; const [searchParams, setSearchParams] = useSearchParams(); // 获取当前页码和标签筛选 @@ -118,9 +122,20 @@ const Timeline = () => {
-

- {t('updates.title')} -

+
+

+ {t('updates.title')} +

+ + + +