mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-06-28 04:06:54 +00:00
create RSS page
This commit is contained in:
parent
32bd9aa678
commit
a8c287d021
92
web/app/api/rss/route.ts
Normal file
92
web/app/api/rss/route.ts
Normal 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",
|
||||
},
|
||||
})
|
||||
}
|
@ -2,13 +2,13 @@ import fs from "fs"
|
||||
import path from "path"
|
||||
import { remark } from "remark"
|
||||
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 React from "react"
|
||||
import parse from "html-react-parser"
|
||||
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 })
|
||||
|
||||
async function getChangelogContent() {
|
||||
@ -16,33 +16,33 @@ async function getChangelogContent() {
|
||||
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md")
|
||||
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
console.error("❌ Archivo CHANGELOG.md no encontrado.")
|
||||
return "<p class='text-red-600'>Error: No se encontró el archivo CHANGELOG.md</p>"
|
||||
console.error("❌ CHANGELOG.md file not found.")
|
||||
return "<p class='text-red-600'>Error: CHANGELOG.md file not found</p>"
|
||||
}
|
||||
|
||||
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()
|
||||
.use(gfm.default || gfm) // ✅ Manejo seguro de `remark-gfm`
|
||||
.use(gfm.default || gfm) // Safe handling of remark-gfm
|
||||
.use(html)
|
||||
.process(fileContents)
|
||||
|
||||
return result.toString()
|
||||
} catch (error) {
|
||||
console.error("❌ Error al leer el archivo CHANGELOG.md", error)
|
||||
return "<p class='text-red-600'>Error: No se pudo cargar el contenido del changelog.</p>"
|
||||
console.error("❌ Error reading CHANGELOG.md file", error)
|
||||
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) {
|
||||
return content.replace(/<code>(.*?)<\/code>/g, (_, codeContent) => {
|
||||
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) {
|
||||
return parse(content, {
|
||||
replace: (domNode: any) => {
|
||||
@ -53,20 +53,24 @@ function wrapCodeBlocksWithCopyable(content: string) {
|
||||
return <CopyableCode code={codeContent} />
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default async function ChangelogPage() {
|
||||
const changelogContent = await getChangelogContent()
|
||||
const cleanedInlineCode = cleanInlineCode(changelogContent) // 🔹 Primero limpiamos código en línea
|
||||
const parsedContent = wrapCodeBlocksWithCopyable(cleanedInlineCode) // 🔹 Luego aplicamos JSX a bloques de código
|
||||
const cleanedInlineCode = cleanInlineCode(changelogContent) // First clean inline code
|
||||
const parsedContent = wrapCodeBlocksWithCopyable(cleanedInlineCode) // Then apply JSX to code blocks
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
<Footer />
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link"
|
||||
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"
|
||||
|
||||
export default function Navbar() {
|
||||
@ -43,6 +43,18 @@ export default function Navbar() {
|
||||
<span>{item.label}</span>
|
||||
</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>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
@ -66,6 +78,19 @@ export default function Navbar() {
|
||||
<span>{item.label}</span>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
|
22
web/components/rss-link.tsx
Normal file
22
web/components/rss-link.tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user