Skip to content

Commit 797c16c

Browse files
committed
docs: add release notes feed
1 parent 7093f05 commit 797c16c

File tree

7 files changed

+181
-1
lines changed

7 files changed

+181
-1
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/fetch-docs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function loadDocsPage(
6565
);
6666
}
6767

68-
async function loadDocsPageFromRelativeFilePath(
68+
export async function loadDocsPageFromRelativeFilePath(
6969
relativeFilePath: string,
7070
): Promise<DocsPageData> {
7171
const mdxFileContent = matter.read(relativeFilePath);

src/lib/generate-feed.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
async function mdToHtml(md: string): Promise<string> {
11+
const result = await unified()
12+
.use(remarkParse)
13+
.use(remarkRehype)
14+
.use(rehypeStringify)
15+
.process(md);
16+
17+
return result.toString();
18+
}
19+
20+
const BASE_URL = "https://ghostty.org";
21+
const RELEASE_NOTES_DIRECTORY = "./docs/install/release-notes/";
22+
const FEED_FILENAME = "feed.json";
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+
).map((dirent) => dirent.name);
47+
48+
for (const filename of filenames) {
49+
const filePath = path.join(RELEASE_NOTES_DIRECTORY, filename);
50+
51+
const { data, content } = matter.read(filePath);
52+
const contentHtml = await mdToHtml(content);
53+
54+
const slug = filename.replace(".mdx", "");
55+
56+
const fileUrl = new URL(slug, releaseNotesUrl).href;
57+
const dateString = data.description?.match(
58+
/released on ([A-Za-z]+ \d+, \d{4})/i,
59+
)?.[1];
60+
const date = dateString ? new Date(dateString) : new Date();
61+
62+
feed.addItem({
63+
title: data.title,
64+
id: fileUrl,
65+
link: fileUrl,
66+
description: data.description,
67+
content: contentHtml,
68+
date,
69+
});
70+
}
71+
72+
return feed.json1();
73+
}

0 commit comments

Comments
 (0)