Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-slot": "^1.2.0",
"class-variance-authority": "^0.7.1",
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 @@ -18,6 +18,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