Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build]
command = "npm run build"
command = "npm ci && npm run build"
publish = ".next"

[[plugins]]
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@netlify/blobs": "^8.1.0",
"@netlify/functions": "^3.0.0",
"@octokit/webhooks-types": "^7.6.1",
"marked": "^15.0.9",
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
55 changes: 52 additions & 3 deletions src/app/feed/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -23,13 +67,18 @@ export async function GET() {
<language>en-US</language>
${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 `
<item>
<title><![CDATA[${entry.title}]]></title>
<link>${baseUrl}/entry/${entry.id}</link>
<guid isPermaLink="false">${entry.id}</guid>
<pubDate>${new Date(entry.date).toUTCString()}</pubDate>
<description><![CDATA[${entry.description}]]></description>
<description><![CDATA[${htmlDescription}]]></description>
${
entry.metadata.sourceRepo
? `
Expand All @@ -38,7 +87,7 @@ export async function GET() {
: ""
}
</item>
`,
`}
)
.join("")}
</channel>
Expand Down
13 changes: 13 additions & 0 deletions tests/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ test.describe('API Routes', () => {
expect(body).toContain('<rss');
expect(body).toContain('<channel>');
});

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
Expand Down