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 ` <![CDATA[${entry.title}]]> ${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