From 745a5374fed3fe7515e7c283efa7ce77a4bff92c Mon Sep 17 00:00:00 2001 From: vwkd <33468089+vwkd@users.noreply.github.com> Date: Fri, 20 Jun 2025 01:55:19 +0200 Subject: [PATCH] docs: add release notes feed --- docs/install/release-notes/index.mdx | 2 + package-lock.json | 85 +++++++++++++++++++ package.json | 2 + .../install/release-notes/feed.json/route.ts | 12 +++ src/layouts/root-layout/index.tsx | 6 ++ src/lib/generate-feed.ts | 75 ++++++++++++++++ 6 files changed, 182 insertions(+) create mode 100644 src/app/docs/install/release-notes/feed.json/route.ts create mode 100644 src/lib/generate-feed.ts diff --git a/docs/install/release-notes/index.mdx b/docs/install/release-notes/index.mdx index 6145072f..e768ff99 100644 --- a/docs/install/release-notes/index.mdx +++ b/docs/install/release-notes/index.mdx @@ -10,3 +10,5 @@ description: |- - [1.1.1](/docs/install/release-notes/1-1-1) – Released on February 13, 2025 - [1.1.0](/docs/install/release-notes/1-1-0) – Released on January 30, 2025 - [1.0.1](/docs/install/release-notes/1-0-1) – Released on December 31, 2024 + +You can also subscribe to the [JSON feed](/docs/install/release-notes/feed.json). diff --git a/package-lock.json b/package-lock.json index 982d8e1f..31d5f351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@r4ai/remark-callout": "^0.6.2", "classnames": "^2.5.1", + "feed": "^5.1.0", "gray-matter": "^4.0.3", "klaw-sync": "^6.0.0", "lucide-react": "^0.424.0", @@ -21,6 +22,7 @@ "react-dom": "^18", "react-intersection-observer": "^9.14.0", "rehype-highlight": "^7.0.1", + "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", "slugify": "^1.6.6", "xml2js": "^0.6.2", @@ -3448,6 +3450,19 @@ "reusify": "^1.0.4" } }, + "node_modules/feed": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/feed/-/feed-5.1.0.tgz", + "integrity": "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3922,6 +3937,39 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", @@ -3989,6 +4037,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -7084,6 +7142,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", @@ -8440,6 +8513,18 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index 418907ab..20f2bef3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@r4ai/remark-callout": "^0.6.2", "classnames": "^2.5.1", + "feed": "^5.1.0", "gray-matter": "^4.0.3", "klaw-sync": "^6.0.0", "lucide-react": "^0.424.0", @@ -22,6 +23,7 @@ "react-dom": "^18", "react-intersection-observer": "^9.14.0", "rehype-highlight": "^7.0.1", + "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", "slugify": "^1.6.6", "xml2js": "^0.6.2", diff --git a/src/app/docs/install/release-notes/feed.json/route.ts b/src/app/docs/install/release-notes/feed.json/route.ts new file mode 100644 index 00000000..0509b06e --- /dev/null +++ b/src/app/docs/install/release-notes/feed.json/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { generateFeed } from "@/lib/generate-feed"; + +export async function GET() { + const feed = await generateFeed(); + + return new NextResponse(feed, { + headers: { + "Content-Type": "application/feed+json", + }, + }); +} diff --git a/src/layouts/root-layout/index.tsx b/src/layouts/root-layout/index.tsx index a4506941..b1b4b93d 100644 --- a/src/layouts/root-layout/index.tsx +++ b/src/layouts/root-layout/index.tsx @@ -42,6 +42,12 @@ export default function RootLayout({ + { + const result = await unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeStringify) + .process(md); + + return result.toString(); +} + +export async function generateFeed(): Promise { + const releaseNotesUrl = new URL(RELEASE_NOTES_DIRECTORY, BASE_URL).href; + const feedUrl = new URL(FEED_FILENAME, releaseNotesUrl).href; + const currentYear = new Date().getFullYear(); + + const feed = new Feed({ + title: "Ghostty Release Notes", + description: "Release notes for Ghostty", + id: feedUrl, + link: releaseNotesUrl, + feedLinks: { + json: feedUrl, + }, + favicon: new URL("favicon.ico", BASE_URL).href, + copyright: `© ${currentYear} Mitchell Hashimoto`, + }); + + const releaseNotesDir = path.join(process.cwd(), RELEASE_NOTES_DIRECTORY); + const filenames = (await fs.readdir(releaseNotesDir, { withFileTypes: true })) + .filter((dirent) => + dirent.isFile() && dirent.name !== "index.mdx" && + dirent.name.endsWith(".mdx") + ) + .map((dirent) => dirent.name) + .toReversed(); + + for (const filename of filenames) { + const filePath = path.join(RELEASE_NOTES_DIRECTORY, filename); + + const { data, content } = matter.read(filePath); + const contentHtml = await mdToHtml(content); + + const slug = filename.replace(".mdx", ""); + + const fileUrl = new URL(slug, releaseNotesUrl).href; + const dateString = data.description?.match( + /released on ([A-Za-z]+ \d+, \d{4})/i, + )?.[1]; + const date = dateString ? new Date(dateString) : new Date(); + + feed.addItem({ + title: data.title, + id: fileUrl, + link: fileUrl, + description: data.description, + content: contentHtml, + date, + }); + } + + return feed.json1(); +}