create RSS page

This commit is contained in:
MacRimi 2025-05-27 17:16:44 +02:00
parent 32bd9aa678
commit a8c287d021
4 changed files with 160 additions and 17 deletions

92
web/app/api/rss/route.ts Normal file
View File

@ -0,0 +1,92 @@
import { NextResponse } from "next/server"
import fs from "fs"
import path from "path"
interface ChangelogEntry {
version: string
date: string
content: string
url: string
}
async function parseChangelog(): Promise<ChangelogEntry[]> {
try {
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md")
if (!fs.existsSync(changelogPath)) {
return []
}
const fileContents = fs.readFileSync(changelogPath, "utf8")
const entries: ChangelogEntry[] = []
// Split content by versions (assuming format ## [version] - date)
const sections = fileContents.split(/^## /gm).filter((section) => section.trim())
for (const section of sections) {
const lines = section.split("\n")
const headerLine = lines[0]
// Extract version and date from header
const versionMatch = headerLine.match(/\[([^\]]+)\]/)
const dateMatch = headerLine.match(/(\d{4}-\d{2}-\d{2})/)
if (versionMatch) {
const version = versionMatch[1]
const date = dateMatch ? dateMatch[1] : new Date().toISOString().split("T")[0]
const content = lines.slice(1).join("\n").trim()
entries.push({
version,
date,
content,
url: `${process.env.NEXT_PUBLIC_SITE_URL || "https://macrimi.github.io/ProxMenux"}/changelog#${version}`,
})
}
}
return entries.slice(0, 10) // Latest 10 entries
} catch (error) {
console.error("Error parsing changelog:", error)
return []
}
}
export async function GET() {
const entries = await parseChangelog()
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://macrimi.github.io/ProxMenux"
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>ProxMenux Changelog</title>
<description>Latest updates and changes in ProxMenux</description>
<link>${siteUrl}/changelog</link>
<atom:link href="${siteUrl}/api/rss" rel="self" type="application/rss+xml"/>
<language>en</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<generator>ProxMenux RSS Generator</generator>
${entries
.map(
(entry) => `
<item>
<title>ProxMenux ${entry.version}</title>
<description><![CDATA[${entry.content.substring(0, 500)}...]]></description>
<link>${entry.url}</link>
<guid isPermaLink="true">${entry.url}</guid>
<pubDate>${new Date(entry.date).toUTCString()}</pubDate>
<category>Changelog</category>
</item>`,
)
.join("")}
</channel>
</rss>`
return new NextResponse(rssXml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
})
}

View File

@ -2,13 +2,13 @@ import fs from "fs"
import path from "path" import path from "path"
import { remark } from "remark" import { remark } from "remark"
import html from "remark-html" import html from "remark-html"
import * as gfm from "remark-gfm" // ✅ Asegura la correcta importación de `remark-gfm` import * as gfm from "remark-gfm"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import React from "react"
import parse from "html-react-parser" import parse from "html-react-parser"
import Footer from "@/components/footer" import Footer from "@/components/footer"
import RSSLink from "@/components/rss-link"
// 🔹 Importamos `CopyableCode` dinámicamente para evitar problemas de SSR // Import CopyableCode dynamically to avoid SSR issues
const CopyableCode = dynamic(() => import("@/components/CopyableCode"), { ssr: false }) const CopyableCode = dynamic(() => import("@/components/CopyableCode"), { ssr: false })
async function getChangelogContent() { async function getChangelogContent() {
@ -16,33 +16,33 @@ async function getChangelogContent() {
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md") const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md")
if (!fs.existsSync(changelogPath)) { if (!fs.existsSync(changelogPath)) {
console.error("❌ Archivo CHANGELOG.md no encontrado.") console.error("❌ CHANGELOG.md file not found.")
return "<p class='text-red-600'>Error: No se encontró el archivo CHANGELOG.md</p>" return "<p class='text-red-600'>Error: CHANGELOG.md file not found</p>"
} }
const fileContents = fs.readFileSync(changelogPath, "utf8") const fileContents = fs.readFileSync(changelogPath, "utf8")
// ✅ Agregamos `remark-gfm` para permitir imágenes, tablas y otros elementos avanzados de Markdown // Add remark-gfm to support images, tables and other advanced Markdown elements
const result = await remark() const result = await remark()
.use(gfm.default || gfm) // ✅ Manejo seguro de `remark-gfm` .use(gfm.default || gfm) // Safe handling of remark-gfm
.use(html) .use(html)
.process(fileContents) .process(fileContents)
return result.toString() return result.toString()
} catch (error) { } catch (error) {
console.error("❌ Error al leer el archivo CHANGELOG.md", error) console.error("❌ Error reading CHANGELOG.md file", error)
return "<p class='text-red-600'>Error: No se pudo cargar el contenido del changelog.</p>" return "<p class='text-red-600'>Error: Could not load changelog content.</p>"
} }
} }
// 🔹 Limpia las comillas invertidas en fragmentos de código en línea // Clean backticks in inline code fragments
function cleanInlineCode(content: string) { function cleanInlineCode(content: string) {
return content.replace(/<code>(.*?)<\/code>/g, (_, codeContent) => { return content.replace(/<code>(.*?)<\/code>/g, (_, codeContent) => {
return `<code class="bg-gray-200 text-gray-900 px-1 rounded">${codeContent.replace(/^`|`$/g, "")}</code>` return `<code class="bg-gray-200 text-gray-900 px-1 rounded">${codeContent.replace(/^`|`$/g, "")}</code>`
}) })
} }
// 🔹 Envuelve los bloques de código en <CopyableCode /> // Wrap code blocks with CopyableCode component
function wrapCodeBlocksWithCopyable(content: string) { function wrapCodeBlocksWithCopyable(content: string) {
return parse(content, { return parse(content, {
replace: (domNode: any) => { replace: (domNode: any) => {
@ -53,20 +53,24 @@ function wrapCodeBlocksWithCopyable(content: string) {
return <CopyableCode code={codeContent} /> return <CopyableCode code={codeContent} />
} }
} }
} },
}) })
} }
export default async function ChangelogPage() { export default async function ChangelogPage() {
const changelogContent = await getChangelogContent() const changelogContent = await getChangelogContent()
const cleanedInlineCode = cleanInlineCode(changelogContent) // 🔹 Primero limpiamos código en línea const cleanedInlineCode = cleanInlineCode(changelogContent) // First clean inline code
const parsedContent = wrapCodeBlocksWithCopyable(cleanedInlineCode) // 🔹 Luego aplicamos JSX a bloques de código const parsedContent = wrapCodeBlocksWithCopyable(cleanedInlineCode) // Then apply JSX to code blocks
return ( return (
<div className="min-h-screen bg-white text-gray-900"> <div className="min-h-screen bg-white text-gray-900">
<div className="container mx-auto px-4 py-16" style={{ maxWidth: "980px" }}> {/* 📌 Ajuste exacto como GitHub */} <div className="container mx-auto px-4 py-16" style={{ maxWidth: "980px" }}>
{" "}
{/* Exact adjustment like GitHub */}
<h1 className="text-4xl font-bold mb-8">Changelog</h1> <h1 className="text-4xl font-bold mb-8">Changelog</h1>
<div className="prose max-w-none text-[16px]">{parsedContent}</div> {/* 📌 Texto ajustado a 16px */} {/* RSS Link Component */}
<RSSLink />
<div className="prose max-w-none text-[16px]">{parsedContent}</div> {/* Text adjusted to 16px */}
</div> </div>
<Footer /> <Footer />
</div> </div>

View File

@ -2,7 +2,7 @@
import Link from "next/link" import Link from "next/link"
import Image from "next/image" import Image from "next/image"
import { Book, GitBranch, FileText, Github, Menu } from "lucide-react" import { Book, GitBranch, FileText, Github, Menu, Rss } from "lucide-react"
import { useState } from "react" import { useState } from "react"
export default function Navbar() { export default function Navbar() {
@ -43,6 +43,18 @@ export default function Navbar() {
<span>{item.label}</span> <span>{item.label}</span>
</Link> </Link>
))} ))}
{/* RSS Feed Link */}
<Link
href="/api/rss"
className="flex items-center space-x-2 transition-colors hover:text-primary text-orange-600 hover:text-orange-700"
target="_blank"
rel="noopener noreferrer"
title="RSS Feed del Changelog"
>
<Rss className="h-4 w-4" />
<span>RSS</span>
</Link>
</nav> </nav>
{/* Mobile menu button */} {/* Mobile menu button */}
@ -66,6 +78,19 @@ export default function Navbar() {
<span>{item.label}</span> <span>{item.label}</span>
</Link> </Link>
))} ))}
{/* RSS Feed Link - Mobile */}
<Link
href="/api/rss"
className="flex items-center space-x-2 py-2 transition-colors hover:text-primary text-orange-600 hover:text-orange-700"
onClick={() => setIsMenuOpen(false)}
target="_blank"
rel="noopener noreferrer"
title="RSS Feed del Changelog"
>
<Rss className="h-4 w-4" />
<span>RSS</span>
</Link>
</nav> </nav>
)} )}
</div> </div>

View File

@ -0,0 +1,22 @@
import { Rss } from "lucide-react"
import Link from "next/link"
export default function RSSLink() {
return (
<div className="flex items-center justify-between mb-8 p-4 bg-orange-50 border border-orange-200 rounded-lg">
<div>
<h3 className="text-lg font-semibold text-orange-900">Stay Updated!</h3>
<p className="text-orange-700">Subscribe to our RSS feed to get notified of new changes.</p>
</div>
<Link
href="/api/rss"
className="flex items-center space-x-2 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<Rss className="h-4 w-4" />
<span>RSS Feed</span>
</Link>
</div>
)
}