Skip to content

Commit 745a537

Browse files
committed
docs: add release notes feed
1 parent 7093f05 commit 745a537

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

docs/install/release-notes/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ description: |-
1010
- [1.1.1](/docs/install/release-notes/1-1-1) – Released on February 13, 2025
1111
- [1.1.0](/docs/install/release-notes/1-1-0) – Released on January 30, 2025
1212
- [1.0.1](/docs/install/release-notes/1-0-1) – Released on December 31, 2024
13+
14+
You can also subscribe to the [JSON feed](/docs/install/release-notes/feed.json).

package-lock.json

Lines changed: 85 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@r4ai/remark-callout": "^0.6.2",
1313
"classnames": "^2.5.1",
14+
"feed": "^5.1.0",
1415
"gray-matter": "^4.0.3",
1516
"klaw-sync": "^6.0.0",
1617
"lucide-react": "^0.424.0",
@@ -22,6 +23,7 @@
2223
"react-dom": "^18",
2324
"react-intersection-observer": "^9.14.0",
2425
"rehype-highlight": "^7.0.1",
26+
"rehype-stringify": "^10.0.1",
2527
"remark-gfm": "^4.0.0",
2628
"slugify": "^1.6.6",
2729
"xml2js": "^0.6.2",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextResponse } from "next/server";
2+
import { generateFeed } from "@/lib/generate-feed";
3+
4+
export async function GET() {
5+
const feed = await generateFeed();
6+
7+
return new NextResponse(feed, {
8+
headers: {
9+
"Content-Type": "application/feed+json",
10+
},
11+
});
12+
}

src/layouts/root-layout/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export default function RootLayout({
4242
<meta property="og:image:width" content="1800" />
4343
<meta property="og:image:height" content="3200" />
4444

45+
<link
46+
rel="alternate"
47+
type="application/feed+json"
48+
title="Ghostty Release Notes"
49+
href="/docs/install/release-notes/feed.json"
50+
/>
4551
<link
4652
rel="icon"
4753
type="image/png"

src/lib/generate-feed.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { promises as fs } from "fs";
2+
import path from "path";
3+
import matter from "gray-matter";
4+
import { Feed } from "feed";
5+
import { unified } from "unified";
6+
import remarkParse from "remark-parse";
7+
import remarkRehype from "remark-rehype";
8+
import rehypeStringify from "rehype-stringify";
9+
10+
const BASE_URL = "https://ghostty.org";
11+
const RELEASE_NOTES_DIRECTORY = "./docs/install/release-notes/";
12+
const FEED_FILENAME = "feed.json";
13+
14+
async function mdToHtml(md: string): Promise<string> {
15+
const result = await unified()
16+
.use(remarkParse)
17+
.use(remarkRehype)
18+
.use(rehypeStringify)
19+
.process(md);
20+
21+
return result.toString();
22+
}
23+
24+
export async function generateFeed(): Promise<string> {
25+
const releaseNotesUrl = new URL(RELEASE_NOTES_DIRECTORY, BASE_URL).href;
26+
const feedUrl = new URL(FEED_FILENAME, releaseNotesUrl).href;
27+
const currentYear = new Date().getFullYear();
28+
29+
const feed = new Feed({
30+
title: "Ghostty Release Notes",
31+
description: "Release notes for Ghostty",
32+
id: feedUrl,
33+
link: releaseNotesUrl,
34+
feedLinks: {
35+
json: feedUrl,
36+
},
37+
favicon: new URL("favicon.ico", BASE_URL).href,
38+
copyright: ${currentYear} Mitchell Hashimoto`,
39+
});
40+
41+
const releaseNotesDir = path.join(process.cwd(), RELEASE_NOTES_DIRECTORY);
42+
const filenames = (await fs.readdir(releaseNotesDir, { withFileTypes: true }))
43+
.filter((dirent) =>
44+
dirent.isFile() && dirent.name !== "index.mdx" &&
45+
dirent.name.endsWith(".mdx")
46+
)
47+
.map((dirent) => dirent.name)
48+
.toReversed();
49+
50+
for (const filename of filenames) {
51+
const filePath = path.join(RELEASE_NOTES_DIRECTORY, filename);
52+
53+
const { data, content } = matter.read(filePath);
54+
const contentHtml = await mdToHtml(content);
55+
56+
const slug = filename.replace(".mdx", "");
57+
58+
const fileUrl = new URL(slug, releaseNotesUrl).href;
59+
const dateString = data.description?.match(
60+
/released on ([A-Za-z]+ \d+, \d{4})/i,
61+
)?.[1];
62+
const date = dateString ? new Date(dateString) : new Date();
63+
64+
feed.addItem({
65+
title: data.title,
66+
id: fileUrl,
67+
link: fileUrl,
68+
description: data.description,
69+
content: contentHtml,
70+
date,
71+
});
72+
}
73+
74+
return feed.json1();
75+
}

0 commit comments

Comments
 (0)