Conversation
Add a changelog feature similar to Linear's changelog to the marketing site. - Add changelog lib utilities (changelog.ts, changelog-utils.ts) - Create changelog list page (/changelog) - Create individual changelog entry pages (/changelog/[slug]) - Add ChangelogCard and ChangelogEntryLayout components - Add changelog to header navigation - Create RSS feed at /changelog.xml - Update sitemap to include changelog pages - Add example changelog entry
📝 WalkthroughWalkthroughThis PR introduces a comprehensive changelog feature for the marketing application, including route handlers for RSS feed generation, page components for displaying changelog entries, reusable UI components for changelog cards and entries, utility functions for parsing and formatting changelog data, navigation integration, sitemap updates, and multiple new changelog content entries documenting recent product updates. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/marketing/content/changelog/2026-01-27-january-2026-release.mdx`:
- Around line 1-6: The frontmatter in the changelog references a missing image
(/images/changelog/january-2026.png); either add the PNG asset under
apps/marketing/public/images/changelog named january-2026.png or update the
frontmatter image value in 2026-01-27-january-2026-release.mdx to the correct
existing image path; ensure the filename and path match the site/public image
serving convention so the image resolves at runtime.
In `@apps/marketing/src/app/changelog/page.tsx`:
- Around line 73-77: Replace the React list key to use the canonical identifier:
in the entries.map(...) that renders <ChangelogCard key={entry.url}
entry={entry} /> change the key to entry.slug so the mapping uses the same
identifier used by getChangelogEntry, getAllChangelogSlugs, and the dynamic
[slug] route; update the key expression from entry.url to entry.slug in the map
callback.
In `@apps/marketing/src/lib/changelog.ts`:
- Around line 14-42: The parseFrontmatter function currently swallows errors in
its catch block; update its error handling to catch the exception (e.g., catch
(err)) and log a clear, contextual message including the filePath and the
error/stack (for example: "Error parsing frontmatter for <filePath>: <err>")
before returning null so failures like permission issues or malformed
frontmatter are visible; ensure you modify parseFrontmatter's catch clause only
(no behavioral changes to returned value) and use the existing logging approach
in the project (console.error or the project's logger) for consistency.
🧹 Nitpick comments (7)
apps/marketing/src/app/changelog/components/ChangelogCard/ChangelogCard.tsx (1)
18-27: Consider addingsizesprop to optimize image loading.When using
fillwith Next.js Image, providing asizesprop helps the browser select the appropriate image size for the viewport, reducing unnecessary bandwidth usage.Suggested improvement
<Image src={entry.image} alt={entry.title} fill + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" className="object-cover transition-transform group-hover:scale-[1.02]" />apps/marketing/src/app/changelog/[slug]/components/ChangelogEntryLayout/ChangelogEntryLayout.tsx (2)
27-34: Consider extracting complex background gradient to a CSS class.The inline style with multiple
calc()expressions is hard to maintain. This could be extracted to a Tailwind arbitrary value or a CSS custom property for better readability.
83-96: Consider addingsizesprop to optimize image loading.Similar to
ChangelogCard, providing asizesprop helps the browser select the appropriate image size.Suggested improvement
<Image src={entry.image} alt={entry.title} fill className="object-cover" priority + sizes="(max-width: 768px) 100vw, 768px" />apps/marketing/src/app/changelog/[slug]/page.tsx (1)
26-33: Minor inconsistency in date formatting.On line 30,
publishedTimeis converted to ISO format usingnew Date(entry.date).toISOString(), but ingenerateMetadata(line 69),publishedTimeis passed asentry.datedirectly without conversion. This inconsistency could lead to different date formats in structured data vs. OpenGraph metadata.🔧 Suggested fix for consistency
openGraph: { title: entry.title, description: entry.description, type: "article", url, siteName: COMPANY.NAME, - publishedTime: entry.date, + publishedTime: new Date(entry.date).toISOString(), ...(entry.image && { images: [entry.image] }), },apps/marketing/src/app/changelog.xml/route.ts (2)
40-45: Consider usingapplication/rss+xmlContent-Type for RSS feeds.While
application/xmlworks,application/rss+xmlis the more semantically correct MIME type for RSS feeds and helps feed readers identify the content type more precisely.🔧 Suggested change
return new Response(rss, { headers: { - "Content-Type": "application/xml", + "Content-Type": "application/rss+xml; charset=utf-8", "Cache-Control": "public, max-age=3600, s-maxage=3600", }, });
8-14: Consider extractingescapeXmlto a shared utility.This XML escaping function is defined inline within the route handler. If XML escaping is needed elsewhere (e.g., other RSS feeds or XML exports), consider moving it to a shared utility module for reusability.
apps/marketing/src/app/changelog/page.tsx (1)
38-45: Magic pixel values in gradient could be extracted to named constants.The values
384pxand383pxappear to define guide line positions relative to the center. Per coding guidelines, consider extracting these to named constants at module top to clarify their purpose (e.g.,CONTENT_WIDTH_PX).
| <div className="flex flex-col gap-8"> | ||
| {entries.map((entry) => ( | ||
| <ChangelogCard key={entry.url} entry={entry} /> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find ChangelogEntry type definition
ast-grep --pattern 'type ChangelogEntry = $_'
ast-grep --pattern 'interface ChangelogEntry $_'
# Check for ChangelogEntry in the changelog lib directory
rg -n "ChangelogEntry" --type ts -A 15 apps/marketing/srcRepository: superset-sh/superset
Length of output: 15480
🏁 Script executed:
#!/bin/bash
# Read the full parseFrontmatter function
sed -n '14,43p' apps/marketing/src/lib/changelog.tsRepository: superset-sh/superset
Length of output: 772
🏁 Script executed:
#!/bin/bash
# Check if url is ever read from frontmatter data or always derived
grep -n "data.url\|url:" apps/marketing/src/lib/changelog.tsRepository: superset-sh/superset
Length of output: 96
Use entry.slug as the React key instead of entry.url.
The entry.url is always derived as /changelog/${slug}, making it stable. However, entry.slug is the canonical identifier throughout the codebase—it's used by getChangelogEntry(slug), getAllChangelogSlugs(), and the dynamic route [slug]. Using slug as the key is semantically more accurate since it represents what actually identifies each entry in the data model.
🤖 Prompt for AI Agents
In `@apps/marketing/src/app/changelog/page.tsx` around lines 73 - 77, Replace the
React list key to use the canonical identifier: in the entries.map(...) that
renders <ChangelogCard key={entry.url} entry={entry} /> change the key to
entry.slug so the mapping uses the same identifier used by getChangelogEntry,
getAllChangelogSlugs, and the dynamic [slug] route; update the key expression
from entry.url to entry.slug in the map callback.
| function parseFrontmatter(filePath: string): ChangelogEntry | null { | ||
| try { | ||
| const fileContent = fs.readFileSync(filePath, "utf-8"); | ||
| const { data, content } = matter(fileContent); | ||
|
|
||
| const slug = path.basename(filePath, ".mdx"); | ||
|
|
||
| let dateValue: string; | ||
| if (data.date instanceof Date) { | ||
| dateValue = data.date.toISOString().split("T")[0] as string; | ||
| } else if (data.date) { | ||
| dateValue = String(data.date); | ||
| } else { | ||
| dateValue = new Date().toISOString().split("T")[0] as string; | ||
| } | ||
|
|
||
| return { | ||
| slug, | ||
| url: `/changelog/${slug}`, | ||
| title: data.title ?? "Untitled", | ||
| description: data.description, | ||
| date: dateValue, | ||
| image: data.image, | ||
| content, | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Silent error swallowing in catch block.
The empty catch block swallows all errors without logging, which could hide issues like file permission errors, malformed frontmatter, or encoding problems. Per coding guidelines, errors should at minimum be logged with context.
Proposed fix
function parseFrontmatter(filePath: string): ChangelogEntry | null {
try {
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
const slug = path.basename(filePath, ".mdx");
let dateValue: string;
if (data.date instanceof Date) {
dateValue = data.date.toISOString().split("T")[0] as string;
} else if (data.date) {
dateValue = String(data.date);
} else {
dateValue = new Date().toISOString().split("T")[0] as string;
}
return {
slug,
url: `/changelog/${slug}`,
title: data.title ?? "Untitled",
description: data.description,
date: dateValue,
image: data.image,
content,
};
- } catch {
+ } catch (error) {
+ console.error(`[changelog/parseFrontmatter] Failed to parse ${filePath}:`, error);
return null;
}
}🤖 Prompt for AI Agents
In `@apps/marketing/src/lib/changelog.ts` around lines 14 - 42, The
parseFrontmatter function currently swallows errors in its catch block; update
its error handling to catch the exception (e.g., catch (err)) and log a clear,
contextual message including the filePath and the error/stack (for example:
"Error parsing frontmatter for <filePath>: <err>") before returning null so
failures like permission issues or malformed frontmatter are visible; ensure you
modify parseFrontmatter's catch clause only (no behavioral changes to returned
value) and use the existing logging approach in the project (console.error or
the project's logger) for consistency.
🚀 Preview Deployment🔗 Preview Links
Preview updates automatically with new commits |
Show full changelog entries with MDX content directly on the list page instead of linking to separate detail pages. This matches Linear's changelog design with date headers and full content inline.
Add a sticky timeline sidebar that follows as you scroll through changelog entries. The timeline shows dates with visual indicators and highlights the currently visible entry. Clicking a date smoothly scrolls to that entry.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@apps/marketing/src/app/changelog/components/ChangelogEntry/changelog-mdx-components.tsx`:
- Around line 3-27: ChangelogLink currently spreads {...props} after the badge
className, allowing props.className to overwrite the badge styles; update
ChangelogLink to merge the default badge classes with any incoming
props.className (e.g., use a utility like clsx or classnames) and pass the
combined className to the <a> so custom classes extend rather than replace the
badge styling; ensure you spread the remaining props (excluding className) after
setting className to the merged value in the returned element.
🧹 Nitpick comments (1)
apps/marketing/src/app/changelog/components/ChangelogEntry/changelog-mdx-components.tsx (1)
1-33: Align components folder structure with the “one component per file” rule.This file defines
ChangelogLinkplus a component map export under acomponentspath. Consider movingChangelogLinkintoChangelogLink/ChangelogLink.tsxwith a barrel export, and keep the MDX mapping in a non-components module (or its own folder) to match the folder convention. As per coding guidelines, please keep one component per file incomponentspaths.
| function ChangelogLink({ | ||
| href, | ||
| children, | ||
| ...props | ||
| }: React.AnchorHTMLAttributes<HTMLAnchorElement>) { | ||
| const isGitHubPR = | ||
| href?.includes("github.com") && href?.includes("/pull/"); | ||
|
|
||
| if (isGitHubPR) { | ||
| return ( | ||
| <a | ||
| href={href} | ||
| className="text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded ml-1 opacity-70 hover:opacity-100 transition-opacity" | ||
| {...props} | ||
| > | ||
| {children} | ||
| </a> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <a href={href} {...props}> | ||
| {children} | ||
| </a> | ||
| ); |
There was a problem hiding this comment.
Avoid losing badge styles when className is provided.
Line 15–16: spreading {...props} after className allows props.className to override the badge styles. Consider merging classes explicitly so custom classes extend (not replace) the badge styling.
🛠️ Proposed fix
-function ChangelogLink({
- href,
- children,
- ...props
-}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
+function ChangelogLink({
+ href,
+ children,
+ className,
+ ...props
+}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const isGitHubPR =
href?.includes("github.com") && href?.includes("/pull/");
if (isGitHubPR) {
return (
<a
- href={href}
- className="text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded ml-1 opacity-70 hover:opacity-100 transition-opacity"
- {...props}
+ {...props}
+ href={href}
+ className={[
+ "text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded ml-1 opacity-70 hover:opacity-100 transition-opacity",
+ className,
+ ]
+ .filter(Boolean)
+ .join(" ")}
>
{children}
</a>
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function ChangelogLink({ | |
| href, | |
| children, | |
| ...props | |
| }: React.AnchorHTMLAttributes<HTMLAnchorElement>) { | |
| const isGitHubPR = | |
| href?.includes("github.com") && href?.includes("/pull/"); | |
| if (isGitHubPR) { | |
| return ( | |
| <a | |
| href={href} | |
| className="text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded ml-1 opacity-70 hover:opacity-100 transition-opacity" | |
| {...props} | |
| > | |
| {children} | |
| </a> | |
| ); | |
| } | |
| return ( | |
| <a href={href} {...props}> | |
| {children} | |
| </a> | |
| ); | |
| function ChangelogLink({ | |
| href, | |
| children, | |
| className, | |
| ...props | |
| }: React.AnchorHTMLAttributes<HTMLAnchorElement>) { | |
| const isGitHubPR = | |
| href?.includes("github.com") && href?.includes("/pull/"); | |
| if (isGitHubPR) { | |
| return ( | |
| <a | |
| {...props} | |
| href={href} | |
| className={[ | |
| "text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded ml-1 opacity-70 hover:opacity-100 transition-opacity", | |
| className, | |
| ] | |
| .filter(Boolean) | |
| .join(" ")} | |
| > | |
| {children} | |
| </a> | |
| ); | |
| } | |
| return ( | |
| <a href={href} {...props}> | |
| {children} | |
| </a> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In
`@apps/marketing/src/app/changelog/components/ChangelogEntry/changelog-mdx-components.tsx`
around lines 3 - 27, ChangelogLink currently spreads {...props} after the badge
className, allowing props.className to overwrite the badge styles; update
ChangelogLink to merge the default badge classes with any incoming
props.className (e.g., use a utility like clsx or classnames) and pass the
combined className to the <a> so custom classes extend rather than replace the
badge styling; ensure you spread the remaining props (excluding className) after
setting className to the merged value in the returned element.
- Create dedicated PRBadge component for consistent PR link styling - Move PR badges to section titles for cleaner layout - Update all changelog entries to use PRBadge component - Use changelogMdxComponents in individual changelog pages
Fetch and apply release notes from linked PRs for all changelog entries. Content now reflects actual feature descriptions from CodeRabbit summaries.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/marketing/src/app/changelog/components/PRBadge/PRBadge.tsx`:
- Around line 5-14: The PRBadge component extracts prNumber via
url.match(/\/pull\/(\d+)/) which can be undefined; update PRBadge to handle
non-matching URLs by either returning null (early return) or rendering a safe
fallback label (e.g., "PR" or the full url) instead of "#undefined". Locate the
PRBadge function and the prNumber variable and add a guard that checks if
prNumber is truthy before rendering "#{prNumber}" (or substitute the chosen
fallback), ensuring links and accessibility remain intact (href stays url or is
omitted when returning null).
| export function PRBadge({ url }: PRBadgeProps) { | ||
| const prNumber = url.match(/\/pull\/(\d+)/)?.[1]; | ||
|
|
||
| return ( | ||
| <a | ||
| href={url} | ||
| className="text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded opacity-70 hover:opacity-100 transition-opacity" | ||
| > | ||
| #{prNumber} | ||
| </a> |
There was a problem hiding this comment.
Avoid rendering “#undefined” for non-matching URLs.
Add a safe fallback label (or early return) when the PR number can’t be parsed.
Suggested fix
export function PRBadge({ url }: PRBadgeProps) {
const prNumber = url.match(/\/pull\/(\d+)/)?.[1];
+ const label = prNumber ? `#${prNumber}` : "PR";
return (
<a
href={url}
className="text-xs font-mono text-muted-foreground no-underline bg-muted px-1.5 py-0.5 rounded opacity-70 hover:opacity-100 transition-opacity"
>
- #{prNumber}
+ {label}
</a>
);
}🤖 Prompt for AI Agents
In `@apps/marketing/src/app/changelog/components/PRBadge/PRBadge.tsx` around lines
5 - 14, The PRBadge component extracts prNumber via url.match(/\/pull\/(\d+)/)
which can be undefined; update PRBadge to handle non-matching URLs by either
returning null (early return) or rendering a safe fallback label (e.g., "PR" or
the full url) instead of "#undefined". Locate the PRBadge function and the
prNumber variable and add a guard that checks if prNumber is truthy before
rendering "#{prNumber}" (or substitute the chosen fallback), ensuring links and
accessibility remain intact (href stays url or is omitted when returning null).
Rename files to reflect their actual titles/content: - 2025-12-23-sqlite-sentry-features - 2026-01-06-sidebar-workspaces-status - 2026-01-13-task-list-shell-env - 2026-01-20-changes-org-settings
Summary
Add a comprehensive changelog page to the marketing site with Linear-style inline layout and sticky timeline navigation.
Features
Changelog Entries
Updated all 7 changelog entries with:
Images Added
ghostty.png- Ghostty terminal integrationchanges.png- Expanded changes sidebartask-list.png- Task list viewbranches.png- Sidebar/workspacesintegrations.png- Linear integrationdocs.png- Documentation siteterminal.png- Multiple terminal tabsports.png- Port visibility panelBug Fixes
Test Plan
/changelog- verify timeline layout and sticky navigationSummary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.