diff --git a/netlify.toml b/netlify.toml
index cd6e3c4..168318b 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -1,5 +1,5 @@
[build]
-command = "npm run build"
+command = "npm ci && npm run build"
publish = ".next"
[[plugins]]
diff --git a/package-lock.json b/package-lock.json
index 438257e..05b2bb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@netlify/blobs": "^8.1.0",
"@netlify/functions": "^3.0.0",
"@octokit/webhooks-types": "^7.6.1",
+ "marked": "^15.0.9",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-toggle": "^1.1.6",
@@ -4532,6 +4533,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "15.0.9",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.9.tgz",
+ "integrity": "sha512-9AW/bn9DxQeZVjR52l5jsc0W2pwuhP04QaQewPvylil12Cfr2GBfWmgp6mu8i9Jy8UlBjqDZ9uMTDuJ8QOGZJA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
diff --git a/package.json b/package.json
index 5abaf32..4ab9584 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@netlify/blobs": "^8.1.0",
"@netlify/functions": "^3.0.0",
"@octokit/webhooks-types": "^7.6.1",
+ "marked": "^15.0.9",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-slot": "^1.2.0",
"class-variance-authority": "^0.7.1",
diff --git a/src/app/api/test/route.ts b/src/app/api/test/route.ts
index 28a225f..56a22ab 100644
--- a/src/app/api/test/route.ts
+++ b/src/app/api/test/route.ts
@@ -17,7 +17,7 @@ export async function POST() {
const testEntry: ChangelogEntry = {
id: Date.now(), // Use timestamp as ID for test entries
title: `Test Entry ${new Date().toISOString()}`,
- description: "This is a test entry to verify cache revalidation",
+ description: "This is a test entry with **bold text**, *italic* and a [link text](http://example.com) to verify markdown handling in RSS feed.",
date: new Date().toISOString(),
metadata: {
sourceRepo: "open-telemetry/test-repo",
diff --git a/src/app/feed/route.ts b/src/app/feed/route.ts
index f7ab01f..ccd07e8 100644
--- a/src/app/feed/route.ts
+++ b/src/app/feed/route.ts
@@ -4,10 +4,54 @@
*/
import { getAllEntries } from "@/lib/store";
+import { marked } from "marked";
export const dynamic = "force-dynamic";
export const revalidate = 60;
+// Convert markdown to HTML for RSS feeds
+function markdownToHtml(markdown: string): string {
+ try {
+ // Use marked.parse with synchronous option to ensure it returns a string
+ return marked.parse(markdown, { async: false }) as string;
+ } catch (error) {
+ console.warn("Failed to convert markdown to HTML:", error);
+ return markdown;
+ }
+}
+
+// Process GitHub-specific markdown like PR references, commit SHAs, etc.
+function processGitHubMarkdown(text: string, repoFullName: string): string {
+ let processedText = text;
+
+ // Replace PR references first (including those in parentheses)
+ const prPattern = /(?:^|\s|[([])#(\d+)(?=[\s\n\])]|$)/g;
+ processedText = processedText.replace(prPattern, (match, issue) => {
+ const prefix = match.startsWith("(") || match.startsWith("[") ? match[0] : " ";
+ return `${prefix}[#${issue}](https://github.com/${repoFullName}/issues/${issue})`;
+ });
+
+ // Replace commit SHAs
+ const shaPattern = /(\s|^)([0-9a-f]{40})(?=[\s\n]|$)/g;
+ processedText = processedText.replace(shaPattern, (match, space, sha) => {
+ return `${space}[${sha}](https://github.com/${repoFullName}/commit/${sha})`;
+ });
+
+ // Replace user mentions
+ const userPattern = /(?:^|\s)@([a-zA-Z0-9-]+)(?=[\s\n]|$)/g;
+ processedText = processedText.replace(userPattern, (match, username) => {
+ return ` [@${username}](https://github.com/${username})`;
+ });
+
+ // Replace repository references with issue numbers
+ const repoPattern = /([a-zA-Z0-9-]+\/[a-zA-Z0-9-._-]+)#(\d+)(?=[\s\n]|$)/g;
+ processedText = processedText.replace(repoPattern, (match, repo, issue) => {
+ return `[${repo}#${issue}](https://github.com/${repo}/issues/${issue})`;
+ });
+
+ return processedText;
+}
+
export async function GET() {
const entries = await getAllEntries();
const baseUrl =
@@ -23,13 +67,18 @@ export async function GET() {
en-US
${entries
.map(
- (entry) => `
+ (entry) => {
+ // Process GitHub markdown first, then convert to HTML
+ const processedDescription = processGitHubMarkdown(entry.description, entry.metadata.sourceRepo);
+ const htmlDescription = markdownToHtml(processedDescription);
+
+ return `
-
${baseUrl}/entry/${entry.id}
${entry.id}
${new Date(entry.date).toUTCString()}
-
+
${
entry.metadata.sourceRepo
? `
@@ -38,7 +87,7 @@ export async function GET() {
: ""
}
- `,
+ `}
)
.join("")}
diff --git a/tests/api.spec.ts b/tests/api.spec.ts
index 6f9362a..03ebc67 100644
--- a/tests/api.spec.ts
+++ b/tests/api.spec.ts
@@ -18,6 +18,19 @@ test.describe('API Routes', () => {
expect(body).toContain('');
});
+
+ test('feed should handle markdown in descriptions', async ({ request }) => {
+ // This test will pass once we implement the markdown parsing fix
+ const response = await request.get('/feed');
+ expect(response.status()).toBe(200);
+
+ const body = await response.text();
+
+ // The test endpoint adds entries with markdown, let's verify we don't see raw markdown
+ // This will initially fail until we fix the issue
+ expect(body).not.toMatch(/\*\*bold text\*\*/);
+ expect(body).not.toMatch(/\[link text\]\(http:\/\/example\.com\)/);
+ });
test('test API should work in development', async ({ request }) => {
// When testing in CI, we're likely in development mode