Skip to content

Commit 0680039

Browse files
austinlparkerclaude
andcommitted
Fix RSS feed markdown handling (#10)
Added markdown-to-HTML conversion for RSS feed descriptions using marked library. Also added tests to verify that markdown is properly converted in the RSS feed. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 540b7fb commit 0680039

File tree

5 files changed

+79
-4
lines changed

5 files changed

+79
-4
lines changed

package-lock.json

Lines changed: 13 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@netlify/blobs": "^8.1.0",
1717
"@netlify/functions": "^3.0.0",
1818
"@octokit/webhooks-types": "^7.6.1",
19+
"marked": "^15.0.9",
1920
"next": "15.3.1",
2021
"react": "^19.0.0",
2122
"react-dom": "^19.0.0",

src/app/api/test/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export async function POST() {
1717
const testEntry: ChangelogEntry = {
1818
id: Date.now(), // Use timestamp as ID for test entries
1919
title: `Test Entry ${new Date().toISOString()}`,
20-
description: "This is a test entry to verify cache revalidation",
20+
description: "This is a test entry with **bold text**, *italic* and a [link text](http://example.com) to verify markdown handling in RSS feed.",
2121
date: new Date().toISOString(),
2222
metadata: {
2323
sourceRepo: "open-telemetry/test-repo",

src/app/feed/route.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,53 @@
44
*/
55

66
import { getAllEntries } from "@/lib/store";
7+
import { marked } from "marked";
78

89
export const dynamic = "force-dynamic";
910
export const revalidate = 60;
1011

12+
// Convert markdown to HTML for RSS feeds
13+
function markdownToHtml(markdown: string): string {
14+
try {
15+
return marked.parse(markdown);
16+
} catch (error) {
17+
console.warn("Failed to convert markdown to HTML:", error);
18+
return markdown;
19+
}
20+
}
21+
22+
// Process GitHub-specific markdown like PR references, commit SHAs, etc.
23+
function processGitHubMarkdown(text: string, repoFullName: string): string {
24+
let processedText = text;
25+
26+
// Replace PR references first (including those in parentheses)
27+
const prPattern = /(?:^|\s|[([])#(\d+)(?=[\s\n\])]|$)/g;
28+
processedText = processedText.replace(prPattern, (match, issue) => {
29+
const prefix = match.startsWith("(") || match.startsWith("[") ? match[0] : " ";
30+
return `${prefix}[#${issue}](https://github.com/${repoFullName}/issues/${issue})`;
31+
});
32+
33+
// Replace commit SHAs
34+
const shaPattern = /(\s|^)([0-9a-f]{40})(?=[\s\n]|$)/g;
35+
processedText = processedText.replace(shaPattern, (match, space, sha) => {
36+
return `${space}[${sha}](https://github.com/${repoFullName}/commit/${sha})`;
37+
});
38+
39+
// Replace user mentions
40+
const userPattern = /(?:^|\s)@([a-zA-Z0-9-]+)(?=[\s\n]|$)/g;
41+
processedText = processedText.replace(userPattern, (match, username) => {
42+
return ` [@${username}](https://github.com/${username})`;
43+
});
44+
45+
// Replace repository references with issue numbers
46+
const repoPattern = /([a-zA-Z0-9-]+\/[a-zA-Z0-9-._-]+)#(\d+)(?=[\s\n]|$)/g;
47+
processedText = processedText.replace(repoPattern, (match, repo, issue) => {
48+
return `[${repo}#${issue}](https://github.com/${repo}/issues/${issue})`;
49+
});
50+
51+
return processedText;
52+
}
53+
1154
export async function GET() {
1255
const entries = await getAllEntries();
1356
const baseUrl =
@@ -23,13 +66,18 @@ export async function GET() {
2366
<language>en-US</language>
2467
${entries
2568
.map(
26-
(entry) => `
69+
(entry) => {
70+
// Process GitHub markdown first, then convert to HTML
71+
const processedDescription = processGitHubMarkdown(entry.description, entry.metadata.sourceRepo);
72+
const htmlDescription = markdownToHtml(processedDescription);
73+
74+
return `
2775
<item>
2876
<title><![CDATA[${entry.title}]]></title>
2977
<link>${baseUrl}/entry/${entry.id}</link>
3078
<guid isPermaLink="false">${entry.id}</guid>
3179
<pubDate>${new Date(entry.date).toUTCString()}</pubDate>
32-
<description><![CDATA[${entry.description}]]></description>
80+
<description><![CDATA[${htmlDescription}]]></description>
3381
${
3482
entry.metadata.sourceRepo
3583
? `
@@ -38,7 +86,7 @@ export async function GET() {
3886
: ""
3987
}
4088
</item>
41-
`,
89+
`}
4290
)
4391
.join("")}
4492
</channel>

tests/api.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ test.describe('API Routes', () => {
1717
expect(body).toContain('<rss');
1818
expect(body).toContain('<channel>');
1919
});
20+
21+
test('feed should handle markdown in descriptions', async ({ request }) => {
22+
// This test will pass once we implement the markdown parsing fix
23+
const response = await request.get('/feed');
24+
expect(response.status()).toBe(200);
25+
26+
const body = await response.text();
27+
28+
// The test endpoint adds entries with markdown, let's verify we don't see raw markdown
29+
// This will initially fail until we fix the issue
30+
expect(body).not.toMatch(/\*\*bold text\*\*/);
31+
expect(body).not.toMatch(/\[link text\]\(http:\/\/example\.com\)/);
32+
});
2033

2134
test('test API should work in development', async ({ request }) => {
2235
// When testing in CI, we're likely in development mode

0 commit comments

Comments
 (0)