Skip to content

feat: migrate blog from external API to MDX file-based content (#407)#409

Open
akintewe wants to merge 6 commits intoboundlessfi:mainfrom
akintewe:feat/mdx-blog-407
Open

feat: migrate blog from external API to MDX file-based content (#407)#409
akintewe wants to merge 6 commits intoboundlessfi:mainfrom
akintewe:feat/mdx-blog-407

Conversation

@akintewe
Copy link
Contributor

@akintewe akintewe commented Feb 25, 2026

Summary

  • Migrate the blog from external API (/blog-posts) to file-based MDX content in content/blog/
  • Convert blog listing and detail pages to async Next.js server components (SSG), eliminating client-side loading states and improving SEO
  • Replace the useMarkdown hook + 650-line inline CSS renderer with next-mdx-remote + Tailwind Typography (prose classes)
  • Introduce lib/mdx.ts with getAllBlogPosts, getBlogPostBySlug, and getRelatedPosts utilities
  • Add 3 sample MDX posts with YAML frontmatter (welcome-to-boundless, building-on-stellar, milestone-based-funding)
  • Related posts are now computed server-side (tag/category scoring) and passed as props — no more useEffect fetch
  • Client-side search and sort remain unchanged; pagination is now client-side show/hide over the static post list
  • Delete lib/api/blog.ts (fully replaced by lib/mdx.ts)
  • Update app/api/blog/search/route.ts and app/sitemap.ts to use the MDX data source
  • Add dependencies: next-mdx-remote, gray-matter, @tailwindcss/typography

Changes

New files

  • lib/mdx.ts — MDX utility functions and MdxBlogPost type
  • content/blog/welcome-to-boundless.mdx
  • content/blog/building-on-stellar.mdx
  • content/blog/milestone-based-funding.mdx

Modified files

  • app/(landing)/blog/page.tsx — async server component, calls getAllBlogPosts()
  • app/(landing)/blog/[slug]/page.tsxgenerateStaticParams + generateMetadata from MDX; renders via getBlogPostBySlug
  • components/landing-page/blog/BlogPostDetails.tsx — renders compiled MDX via prose div; removes useMarkdown
  • components/landing-page/blog/StreamingBlogGrid.tsx — uses MdxBlogPost[]; client-side visible count pagination
  • components/landing-page/blog/BlogCard.tsx — uses MdxBlogPost
  • components/landing-page/blog/BlogGrid.tsx — uses MdxBlogPost
  • components/landing-page/blog/BlogSection.tsx — synchronous getAllBlogPosts() call
  • components/landing-page/blog/BlogSectionClient.tsx — uses MdxBlogPost
  • app/api/blog/search/route.ts — uses getAllBlogPosts() from lib/mdx
  • app/globals.css — adds @plugin "@tailwindcss/typography" + dark prose CSS variable overrides
  • app/sitemap.tsfetchBlogPostsSitemap now reads MDX files synchronously

Deleted files

  • lib/api/blog.ts — replaced by lib/mdx.ts

Test plan

  • npm run build completes with no errors and statically generates all /blog/[slug] routes
  • /blog page renders the 3 sample posts without a loading flash
  • Clicking a post navigates to pre-rendered HTML (no loading state)
  • view-source:/blog/welcome-to-boundless shows content in the HTML response (SSG confirmed)
  • Search and sort filters work client-side on the blog listing page
  • Related articles section displays posts with matching tags/categories
  • /sitemap.xml includes all 3 blog post URLs
Screenshot 2026-02-25 at 2 00 10 PM
Screen.Recording.2026-02-25.at.1.56.11.PM.1.mp4

Closes #407

Summary by CodeRabbit

  • New Features

    • Related posts shown on individual blog pages.
    • MDX rendering with custom MDX components (code, tables, badges, mermaid placeholder).
    • Three new blog posts published (welcome, milestone-based funding, building on Stellar).
  • Improvements

    • Blog content served from local MDX files for faster, consistent rendering.
    • Blog listing moved server-side with simpler "View More" pagination and smoother navigation.
    • Improved tag/category/search normalization, metadata, sitemap, and typography (dark-prose).
  • Chores

    • Added MDX toolchain and typography plugin; tightened CI/workflow checks.

…lessfi#407)

Replace API-fetched blog posts with statically generated MDX content.
Blog pages now use server components and next-mdx-remote for SSG,
eliminating loading states, improving SEO, and removing the backend dependency.

- Add lib/mdx.ts with getAllBlogPosts, getBlogPostBySlug, getRelatedPosts
- Add content/blog/ with 3 sample MDX posts (frontmatter-driven)
- Convert blog listing and detail pages to async server components
- Replace useMarkdown hook + inline CSS with Tailwind Typography (prose)
- Update BlogCard, BlogGrid, BlogSection, StreamingBlogGrid to MdxBlogPost type
- Update blog search API route and sitemap to use MDX data source
- Delete lib/api/blog.ts (replaced by lib/mdx.ts)
- Add next-mdx-remote, gray-matter, @tailwindcss/typography dependencies
@vercel
Copy link

vercel bot commented Feb 25, 2026

@akintewe is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the API-backed blog with a file-based MDX system: removes legacy blog API, adds lib/mdx utilities, migrates pages/components to use MDX data and compiled content, adds multiple MDX posts, updates search/sitemap, adds MDX rendering components and typography styles, and updates CI/pre-commit checks.

Changes

Cohort / File(s) Summary
MDX Core & Public API
lib/mdx.ts
New MDX utilities and types (MdxBlogPost, MdxBlogPostWithContent), plus getAllBlogPosts(), getBlogPostBySlug(), getRelatedPosts() that read/parse/compile .mdx files.
API Layer Removal
lib/api/blog.ts
Deleted legacy REST-style blog API module and all exported blog functions/types.
Blog Pages
app/(landing)/blog/page.tsx, app/(landing)/blog/[slug]/page.tsx
Listing converted to a server async component using getAllBlogPosts; post page fetches compiled MDX via getBlogPostBySlug, uses getRelatedPosts, simplified metadata and notFound flow.
Search / Sitemap / API route
app/api/blog/search/route.ts, app/sitemap.ts
Switched to MDX source (getAllBlogPosts()); filters normalized to string tags/categories; sitemap generation made synchronous and uses MDX frontmatter.
Components – type/data model changes
components/landing-page/blog/...
BlogCard.tsx, BlogGrid.tsx, BlogPostDetails.tsx, BlogSection.tsx, BlogSectionClient.tsx, StreamingBlogGrid.tsx, MdxComponents.tsx
Components updated to MdxBlogPost shape: tags as strings, publishedAt/slug usage, keys switched to post.slug; detail component accepts relatedPosts; MDX render component map added.
Content additions
content/blog/*.mdx
building-on-stellar.mdx, milestone-based-funding.mdx, welcome-to-boundless.mdx
Adds three new MDX posts with frontmatter and body content.
Styling & Dependencies
app/globals.css, package.json
Adds Tailwind Typography styles and dark-prose overrides; dependencies added: gray-matter, next-mdx-remote, dev dep @tailwindcss/typography.
Client listing changes (streaming/pagination)
components/landing-page/blog/StreamingBlogGrid.tsx, app/(landing)/blog/page.tsx
Removed server-driven pagination props; introduced client-side "Show More" behavior with initialPosts from MDX and simplified loading/empty states.
CI / Pre-commit tweaks
.github/workflows/ci.yml, .github/workflows/pre-commit-validation.yml
Minor CI/script updates: vulnerability check adjustment, skip merge commits, and file-existence guards for checks.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (Browser)
  participant Page as Next.js Page
  participant MDXlib as lib/mdx
  participant FS as FileSystem (content/blog)

  Client->>Page: Request /blog/[slug]
  Page->>MDXlib: getBlogPostBySlug(slug)
  MDXlib->>FS: read `content/blog/{slug}.mdx`
  FS-->>MDXlib: raw MDX + frontmatter
  MDXlib-->>Page: MdxBlogPostWithContent (compiled React content)
  Page->>MDXlib: getRelatedPosts(slug, tags, categories)
  MDXlib-->>Page: related posts list
  Page-->>Client: Rendered HTML with compiled MDX and related posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • 0xdevcollins

Poem

🐰 I hopped from API fields to MDX hay,
Frontmatter carrots guide the way,
Compiled content tucked in my den,
Related hops for every friend,
Tailwind fluffs the meadow — read away!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements core MDX setup, content migration, data fetching utilities, and UI refactoring. However, custom MDX components (Mermaid, Badge) integration remains incomplete per reviewer feedback. Complete wiring of custom MDX components (Mermaid, Badge, table rendering) into the compileMDX function in lib/mdx.ts to enable their use within posts.
Out of Scope Changes check ⚠️ Warning All changes directly support the MDX migration objective. Unrelated updates to CI/CD workflows (.github/workflows) fall outside the PR scope but are minor infrastructure improvements. Separate CI/CD workflow changes (vulnerability audit, commit validation) into a distinct PR to keep this PR focused solely on the blog MDX migration.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: migrating the blog from external API to MDX file-based content.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
components/landing-page/blog/BlogGrid.tsx (2)

98-130: ⚠️ Potential issue | 🟠 Major

Load-more still depends on /api/blog/posts despite static MDX pagination.

This handler reintroduces runtime API calls and fallback delays even though posts are already in-memory/static. It adds avoidable latency and brittle failure paths.

♻️ Suggested simplification (client-only pagination)
-  const handleLoadMore = useCallback(async () => {
-    if (isLoading || !hasMorePosts) return;
-
-    setIsLoading(true);
-
-    try {
-      // Simulate streaming by loading more posts
-      const nextPage = currentPage + 1;
-      const response = await fetch(`/api/blog/posts?page=${nextPage}&limit=12`);
-
-      if (response.ok) {
-        const data = await response.json();
-        setAllPosts(prev => [...prev, ...data.posts]);
-        setCurrentPage(nextPage);
-        setHasMorePosts(data.hasMore);
-        setVisiblePosts(prev => prev + 12);
-      } else {
-        // Fallback to local loading if API fails
-        setTimeout(() => {
-          setVisiblePosts(prev => prev + 12);
-          setIsLoading(false);
-        }, 500);
-      }
-    } catch {
-      // Fallback to local loading if API fails
-      setTimeout(() => {
-        setVisiblePosts(prev => prev + 12);
-        setIsLoading(false);
-      }, 500);
-    } finally {
-      setIsLoading(false);
-    }
-  }, [isLoading, hasMorePosts, currentPage]);
+  const handleLoadMore = useCallback(() => {
+    if (isLoading || !hasMorePostsToShow) return;
+    setIsLoading(true);
+    setVisiblePosts(prev => prev + 12);
+    setCurrentPage(prev => prev + 1);
+    setIsLoading(false);
+  }, [isLoading, hasMorePostsToShow]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogGrid.tsx` around lines 98 - 130, The
load-more handler handleLoadMore should not call /api/blog/posts or use
fallbacks; replace the fetch logic with client-side pagination using the
already-loaded allPosts array: compute nextVisible = Math.min(allPosts.length,
visiblePosts + 12), setVisiblePosts(nextVisible), update currentPage (if you
need pages) and hasMorePosts = nextVisible < allPosts.length, and remove the
try/catch/fetch and artificial setTimeout paths; keep setIsLoading(true) at
start and ensure you setIsLoading(false) only once after updating state (or
remove loading state entirely if instant).

146-155: ⚠️ Potential issue | 🔴 Critical

handleCardClick never navigates to the article.

Line 151 only logs, and the callback never calls router navigation. On the blog grid, “Continue Reading” won’t open the post.

🔧 Proposed fix
 import React, { useState, useCallback, useMemo } from 'react';
+import { useRouter } from 'next/navigation';
@@
 const BlogGrid: React.FC<BlogGridProps> = ({
@@
 }) => {
+  const router = useRouter();
@@
   const handleCardClick = useCallback((slug: string) => {
     setIsNavigating(true);
-    // The navigation will be handled by Next.js Link, but we show loading state
-    // The loading state will be cleared when the page actually navigates
-    // eslint-disable-next-line no-console
-    console.log(`Navigating to blog post: ${slug}`);
-    setTimeout(() => {
-      setIsNavigating(false);
-    }, 2000); // Fallback timeout
-  }, []);
+    router.push(`/blog/${slug}`);
+  }, [router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogGrid.tsx` around lines 146 - 155,
handleCardClick currently only logs and never triggers navigation; fix it by
importing and using Next.js useRouter inside the component, capture router
(const router = useRouter()), and update handleCardClick to call
router.push(slug) (or the correct blog path) after setIsNavigating(true). Also
subscribe to router.events (routeChangeComplete and routeChangeError) to clear
setIsNavigating(false) and remove those listeners in cleanup to avoid memory
leaks; keep the existing fallback setTimeout as secondary. Ensure you reference
handleCardClick, setIsNavigating, router.push and
router.events/routeChangeComplete in your changes.
components/landing-page/blog/BlogPostDetails.tsx (1)

28-35: ⚠️ Potential issue | 🟠 Major

Avoid timezone drift when formatting publishedAt.

new Date('YYYY-MM-DD') parses as UTC per ECMAScript spec, but when displayed via toLocaleDateString() without explicit timezone handling, it renders in the user's local timezone and can appear as the previous day in negative UTC offsets (e.g., US timezones).

💡 Suggested fix
   const formatDate = (dateString: string) => {
-    const date = new Date(dateString);
+    const date = new Date(`${dateString}T00:00:00Z`);
     return date.toLocaleDateString('en-US', {
       year: 'numeric',
       month: 'long',
       day: 'numeric',
+      timeZone: 'UTC',
     });
   };

Also applies to: 121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogPostDetails.tsx` around lines 28 - 35, The
formatDate function can suffer timezone drift because new Date(dateString)
treats 'YYYY-MM-DD' as UTC and toLocaleDateString renders in the user's local
zone; update formatDate (and any other uses like the one at line ~121) to parse
the date string into local date components and construct the Date via new
Date(year, monthIndex, day) (or otherwise normalize to UTC before formatting) so
the displayed date is stable across timezones; modify the formatDate helper to
extract year/month/day from the input string and build the Date that
toLocaleDateString will format consistently.
app/(landing)/blog/[slug]/page.tsx (1)

1-67: ⚠️ Potential issue | 🟡 Minor

CI is blocked by Prettier formatting in this file.

The pipeline output indicates formatting violations; this should be prettified before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/blog/[slug]/page.tsx around lines 1 - 67, Prettier-formatting
failures in this file are blocking CI; run the repository's formatter (e.g., npm
run format or npx prettier --write) on this file and reformat the exports and
function blocks (including generateStaticParams, generateMetadata, and the
BlogPostPage component) so spacing, commas, quotes and JSX formatting conform to
the project's Prettier config, then re-run lint/format checks and commit the
updated file.
🧹 Nitpick comments (2)
components/landing-page/blog/StreamingBlogGrid.tsx (1)

28-29: Avoid freezing initialPosts in local state when it never mutates.

Line 28 stores initialPosts in state without using a setter, which can produce stale data if props ever update. A plain constant is safer and clearer.

♻️ Suggested change
-  const [allPosts] = useState<MdxBlogPost[]>(initialPosts);
+  const allPosts = initialPosts;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/StreamingBlogGrid.tsx` around lines 28 - 29, The
component StreamingBlogGrid currently does const [allPosts] =
useState<MdxBlogPost[]>(initialPosts), which freezes initialPosts into state and
can become stale if props change; replace that useState usage with a plain
constant (e.g., const allPosts = initialPosts) and remove the unused setter
reference so the component reads the latest initialPosts prop, keeping the
existing useState for visibleCount (setVisibleCount) unchanged.
app/(landing)/blog/[slug]/page.tsx (1)

23-24: Avoid compiling MDX in metadata and again during page render.

getBlogPostBySlug compiles MDX content. Calling it in both Line 23 and Line 56 doubles expensive work; metadata only needs frontmatter fields.

⚡ Suggested optimization
 export async function generateMetadata({
   params,
 }: BlogPostPageProps): Promise<Metadata> {
   const { slug } = await params;
-  const post = await getBlogPostBySlug(slug);
+  const post = getAllBlogPosts().find(p => p.slug === slug);

   if (!post) {
     return {
       title: 'Blog Post Not Found | Boundless',
@@
 const BlogPostPage = async ({ params }: BlogPostPageProps) => {
   const { slug } = await params;
   const post = await getBlogPostBySlug(slug);

Also applies to: 56-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/blog/[slug]/page.tsx around lines 23 - 24, The metadata
generation currently calls getBlogPostBySlug(slug) which compiles MDX and causes
duplicate work when the page render also calls getBlogPostBySlug; change
metadata to fetch only frontmatter (either by adding a new helper like
getBlogPostFrontmatterBySlug(slug) or by adding a skipCompilation option to
getBlogPostBySlug) and use that lightweight call in the metadata block, leaving
the full getBlogPostBySlug(slug) (which compiles MDX) only for the page render
(e.g., in the default export function); update call sites to use the new
flag/helper so MDX compilation happens once only during render.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/blog/search/route.ts`:
- Around line 33-35: The tag filter is currently case/whitespace sensitive and
can miss matches; update the filtering to normalize both the incoming query tags
and the post tags before exact matching: parse/split any comma-separated values
in the incoming tags, map the resulting tags to trimmed, lowercased strings
(e.g., normalizedTags), and when filtering filteredPosts use those
normalizedTags to check against post.tags normalized via trimming and
toLowerCase (referencing filteredPosts, tags, and post.tags in route.ts).

In `@components/landing-page/blog/StreamingBlogGrid.tsx`:
- Around line 301-303: The “You’ve reached the end of the blog posts!” message
is shown during filtered/search views because the render only checks
hasMoreToShow and filteredPosts; update the conditional to also ensure no active
filters/search are applied (e.g. add a negated flag like !isFiltering or
!hasActiveFilters or check that filteredPosts length equals the full posts
length). Locate the render that uses hasMoreToShow and filteredPosts and change
the condition to something like: show message only when hasMoreToShow is false,
filteredPosts.length > 0, and there are no active filters/search (use the
existing filter/search state used when computing filteredPosts or add an
isFiltering helper if needed).

In `@content/blog/building-on-stellar.mdx`:
- Around line 1-99: The MDX file fails Prettier; run the project's
Prettier/formatter on this MDX content and commit the changes so CI passes —
specifically format the frontmatter and markdown sections and ensure the Rust
code block (the release_milestone function) is properly fenced and indented,
remove any trailing whitespace, and preserve existing content; run the same
formatter command used by CI (e.g., npm/yarn format or prettier --write) and
re-run linting before pushing.

In `@content/blog/milestone-based-funding.mdx`:
- Line 1: The file content/blog/milestone-based-funding.mdx currently fails
Prettier (the leading '---'/frontmatter line is not formatted); fix by running
your project's formatter (e.g., run prettier --write on
content/blog/milestone-based-funding.mdx) or apply the repo's Prettier rules to
the file (ensure proper frontmatter syntax and a trailing newline), then
re-commit the formatted file so CI stops reporting the Prettier error.

In `@lib/mdx.ts`:
- Around line 1-122: The file fails Prettier formatting; run the project's
Prettier formatter over this file and commit the changes so CI passes.
Specifically, format lib/mdx.ts (containing parseFrontmatter, getAllBlogPosts,
getBlogPostBySlug, getRelatedPosts) using the repo's Prettier config (e.g., npx
prettier --write lib/mdx.ts or your editor's format command) and ensure the
resulting file is lint/format clean before pushing.
- Around line 34-48: The return object in lib/mdx.ts must harden frontmatter
normalization: validate and sanitize categories and tags to ensure elements are
strings (e.g., replace the current Array.isArray(data.categories) ?
data.categories : [] logic with filtering/mapping so only typeof item ===
'string' are returned), and similarly sanitize author.name and author.image
(coerce to string or default to ''), plus ensure readingTime and isFeatured are
coerced to number/boolean respectively; update the object construction that
produces slug, title, excerpt, coverImage, author, categories, tags,
readingTime, and isFeatured so downstream callers (e.g., code that calls
tag.toLowerCase()) cannot receive non-string items.

---

Outside diff comments:
In `@app/`(landing)/blog/[slug]/page.tsx:
- Around line 1-67: Prettier-formatting failures in this file are blocking CI;
run the repository's formatter (e.g., npm run format or npx prettier --write) on
this file and reformat the exports and function blocks (including
generateStaticParams, generateMetadata, and the BlogPostPage component) so
spacing, commas, quotes and JSX formatting conform to the project's Prettier
config, then re-run lint/format checks and commit the updated file.

In `@components/landing-page/blog/BlogGrid.tsx`:
- Around line 98-130: The load-more handler handleLoadMore should not call
/api/blog/posts or use fallbacks; replace the fetch logic with client-side
pagination using the already-loaded allPosts array: compute nextVisible =
Math.min(allPosts.length, visiblePosts + 12), setVisiblePosts(nextVisible),
update currentPage (if you need pages) and hasMorePosts = nextVisible <
allPosts.length, and remove the try/catch/fetch and artificial setTimeout paths;
keep setIsLoading(true) at start and ensure you setIsLoading(false) only once
after updating state (or remove loading state entirely if instant).
- Around line 146-155: handleCardClick currently only logs and never triggers
navigation; fix it by importing and using Next.js useRouter inside the
component, capture router (const router = useRouter()), and update
handleCardClick to call router.push(slug) (or the correct blog path) after
setIsNavigating(true). Also subscribe to router.events (routeChangeComplete and
routeChangeError) to clear setIsNavigating(false) and remove those listeners in
cleanup to avoid memory leaks; keep the existing fallback setTimeout as
secondary. Ensure you reference handleCardClick, setIsNavigating, router.push
and router.events/routeChangeComplete in your changes.

In `@components/landing-page/blog/BlogPostDetails.tsx`:
- Around line 28-35: The formatDate function can suffer timezone drift because
new Date(dateString) treats 'YYYY-MM-DD' as UTC and toLocaleDateString renders
in the user's local zone; update formatDate (and any other uses like the one at
line ~121) to parse the date string into local date components and construct the
Date via new Date(year, monthIndex, day) (or otherwise normalize to UTC before
formatting) so the displayed date is stable across timezones; modify the
formatDate helper to extract year/month/day from the input string and build the
Date that toLocaleDateString will format consistently.

---

Nitpick comments:
In `@app/`(landing)/blog/[slug]/page.tsx:
- Around line 23-24: The metadata generation currently calls
getBlogPostBySlug(slug) which compiles MDX and causes duplicate work when the
page render also calls getBlogPostBySlug; change metadata to fetch only
frontmatter (either by adding a new helper like
getBlogPostFrontmatterBySlug(slug) or by adding a skipCompilation option to
getBlogPostBySlug) and use that lightweight call in the metadata block, leaving
the full getBlogPostBySlug(slug) (which compiles MDX) only for the page render
(e.g., in the default export function); update call sites to use the new
flag/helper so MDX compilation happens once only during render.

In `@components/landing-page/blog/StreamingBlogGrid.tsx`:
- Around line 28-29: The component StreamingBlogGrid currently does const
[allPosts] = useState<MdxBlogPost[]>(initialPosts), which freezes initialPosts
into state and can become stale if props change; replace that useState usage
with a plain constant (e.g., const allPosts = initialPosts) and remove the
unused setter reference so the component reads the latest initialPosts prop,
keeping the existing useState for visibleCount (setVisibleCount) unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 57d7a72 and bae3707.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • app/(landing)/blog/[slug]/page.tsx
  • app/(landing)/blog/page.tsx
  • app/api/blog/search/route.ts
  • app/globals.css
  • app/sitemap.ts
  • components/landing-page/blog/BlogCard.tsx
  • components/landing-page/blog/BlogGrid.tsx
  • components/landing-page/blog/BlogPostDetails.tsx
  • components/landing-page/blog/BlogSection.tsx
  • components/landing-page/blog/BlogSectionClient.tsx
  • components/landing-page/blog/StreamingBlogGrid.tsx
  • content/blog/building-on-stellar.mdx
  • content/blog/milestone-based-funding.mdx
  • content/blog/welcome-to-boundless.mdx
  • lib/api/blog.ts
  • lib/mdx.ts
  • package.json
💤 Files with no reviewable changes (1)
  • lib/api/blog.ts

…idation

- Fix security check to only fail on moderate/high/critical vulnerabilities,
  matching the --audit-level=moderate flag (24 pre-existing low-severity vulns
  in @creit.tech/stellar-wallets-kit transitive deps are not actionable)
- Skip merge commits in commit message validation to prevent false failures
- Fix Prettier formatting in MDX files, lib/mdx.ts, and blog slug page
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/landing-page/blog/BlogCard.tsx (1)

62-63: ⚠️ Potential issue | 🟡 Minor

Raw ISO date string will be displayed to users.

post.publishedAt is stored as '2025-01-15' (see frontmatter). Rendering it directly produces an unformatted date. Consider formatting it before display.

✨ Suggested formatting
-  {post.publishedAt}
+  {new Date(post.publishedAt).toLocaleDateString('en-US', {
+    year: 'numeric',
+    month: 'short',
+    day: 'numeric',
+  })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogCard.tsx` around lines 62 - 63, BlogCard
currently renders the raw ISO string post.publishedAt; update the component
(BlogCard) to parse and format post.publishedAt before rendering (e.g., using
Date constructor or a date library/Intl.DateTimeFormat) and replace the direct
{post.publishedAt} output with the formatted string; ensure you handle missing
or invalid dates (fallback string or omit) and consider extracting the
formatting into a small helper like formatDate to keep the JSX clean.
♻️ Duplicate comments (1)
lib/mdx.ts (1)

44-45: ⚠️ Potential issue | 🟠 Major

Array elements not validated as strings — same past review concern is unresolved.

data.categories and data.tags are accepted as-is if they are arrays, without filtering non-string elements. Downstream callers (e.g., tag.toLowerCase() in search, and tags.includes(t) in getRelatedPosts) will throw or misbehave on non-string entries.

🛡️ Proposed fix (applies identically in both `parseFrontmatter` and `getBlogPostBySlug`)
-    categories: Array.isArray(data.categories) ? data.categories : [],
-    tags: Array.isArray(data.tags) ? data.tags : [],
-    readingTime: data.readingTime ?? 0,
+    categories: Array.isArray(data.categories)
+      ? data.categories.filter((c): c is string => typeof c === 'string')
+      : [],
+    tags: Array.isArray(data.tags)
+      ? data.tags.filter((t): t is string => typeof t === 'string')
+      : [],
+    readingTime: typeof data.readingTime === 'number' ? data.readingTime : 0,

Also applies to: 91-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mdx.ts` around lines 44 - 45, Filter and sanitize data.categories and
data.tags to include only string elements before assigning them (apply same
change in parseFrontmatter and getBlogPostBySlug); the current code accepts any
array and can break callers that call string methods like tag.toLowerCase() or
tags.includes(t). Replace the direct assignment of data.categories/data.tags
with a guarded expression that checks Array.isArray(...) and then filters to
keep only typeof item === 'string' (optionally trimming each string) so
downstream uses of categories/tags are safe.
🧹 Nitpick comments (6)
lib/mdx.ts (2)

51-65: Exported utility functions use function declarations instead of const arrow functions.

getAllBlogPosts, getBlogPostBySlug, and getRelatedPosts are all declared with the function keyword. As per coding guidelines, exported functions in .ts files should use const arrow functions with explicit type annotations.

♻️ Example for `getAllBlogPosts`
-export function getAllBlogPosts(): MdxBlogPost[] {
+export const getAllBlogPosts = (): MdxBlogPost[] => {

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

Also applies to: 67-97, 99-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mdx.ts` around lines 51 - 65, Convert the exported function declarations
to const arrow functions with explicit TypeScript return and parameter types:
replace the `function` declaration for getAllBlogPosts with `const
getAllBlogPosts: () => MdxBlogPost[] = () => { ... }`, and do the same for
getBlogPostBySlug and getRelatedPosts (e.g., include parameter types like `slug:
string` and explicit return types). Keep existing logic, imports, and exported
names identical, and ensure the functions remain exported (export const ...) so
typings and linting conform to the project's guideline.

99-119: getRelatedPosts re-reads all MDX files via getAllBlogPosts().

When a page calls getBlogPostBySlug and then getRelatedPosts, every .mdx file in BLOG_DIR is read and parsed twice. As the blog grows this becomes a measurable I/O overhead on every static render.

Consider either accepting the post list as a parameter or memoising getAllBlogPosts with a module-level cache (safe at build time / in RSC context):

let postsCache: MdxBlogPost[] | null = null;

export function getAllBlogPosts(): MdxBlogPost[] {
  if (postsCache) return postsCache;
  // ...existing logic...
  postsCache = posts.sort(...);
  return postsCache;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mdx.ts` around lines 99 - 119, getRelatedPosts currently calls
getAllBlogPosts() again causing all .mdx files to be reread; fix by avoiding
duplicate I/O either by changing getRelatedPosts to accept the precomputed posts
array (e.g., add an overload or new signature like getRelatedPosts(currentSlug,
tags, categories, posts, limit)) or by memoizing getAllBlogPosts with a
module-level cache (introduce postsCache: MdxBlogPost[] | null and return cached
posts from getAllBlogPosts when set), and update callers (e.g.,
getBlogPostBySlug consumer) to pass the cached list or rely on the memoized
getAllBlogPosts to prevent double-reading.
app/(landing)/blog/[slug]/page.tsx (3)

50-61: BlogPostPage missing explicit return type annotation.

As per coding guidelines, const arrow functions should carry explicit type annotations.

♻️ Proposed fix
-const BlogPostPage = async ({ params }: BlogPostPageProps) => {
+const BlogPostPage = async ({ params }: BlogPostPageProps): Promise<React.ReactElement> => {

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/blog/[slug]/page.tsx around lines 50 - 61, The BlogPostPage
arrow function lacks an explicit return type; update its signature to include a
Promise return type (e.g., change "const BlogPostPage = async ({ params }:
BlogPostPageProps) =>" to "const BlogPostPage = async ({ params }:
BlogPostPageProps): Promise<JSX.Element> =>") so the async server component's
return type is explicit; reference the BlogPostPage identifier and the
BlogPostPageProps type when making this change.

11-12: generateStaticParams is unnecessarily async.

getAllBlogPosts() is synchronous. The async keyword adds overhead without benefit.

♻️ Proposed fix
-export async function generateStaticParams() {
+export function generateStaticParams() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/blog/[slug]/page.tsx around lines 11 - 12, The
generateStaticParams function is marked async but calls the synchronous
getAllBlogPosts(), so remove the unnecessary async keyword from
generateStaticParams (and any associated await usage or unnecessary Promise
handling) so it returns the mapped slug objects synchronously; locate the
generateStaticParams declaration and change it to a plain function that returns
getAllBlogPosts().map(post => ({ slug: post.slug })).

19-19: Wrap getBlogPostBySlug with React.cache() to avoid duplicate MDX compilation during static generation.

getBlogPostBySlug is called in both generateMetadata (line 19) and BlogPostPage (line 52), causing compileMDX to run twice per blog post during static build. Since the function uses file I/O and compileMDX rather than fetch(), Next.js does not automatically deduplicate these calls within the same render pass.

Wrap the function with React's cache() at module level:

Suggested fix
// lib/mdx.ts
import { cache } from 'react';

export const getBlogPostBySlug = cache(async (slug: string): Promise<MdxBlogPostWithContent | null> => {
  // ...existing implementation
});

This ensures both generateMetadata and the page component share a single result per request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/blog/[slug]/page.tsx at line 19, Wrap the module-level
function getBlogPostBySlug with React's cache() so calls from generateMetadata
and BlogPostPage share a single cached result and avoid duplicate
compileMDX/file I/O; update the exported getBlogPostBySlug definition in the
mdx/lib file to be exported as cache(async (slug: string) => { ... }) while
keeping the same signature and internal logic so generateMetadata and
BlogPostPage continue calling getBlogPostBySlug unchanged.
components/landing-page/blog/BlogCard.tsx (1)

87-91: Inline event handler should be extracted to a named handler.

The onClick uses an anonymous arrow function directly. As per coding guidelines, event handlers should start with the 'handle' prefix (e.g., handleClick).

♻️ Proposed refactor
+const handleCardClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+  e.preventDefault();
+  onCardClick?.(post.slug);
+};

 <button
-  onClick={e => {
-    e.preventDefault();
-    onCardClick?.(post.slug);
-  }}
+  onClick={handleCardClick}

As per coding guidelines, "Event handlers should start with 'handle' prefix (e.g., handleClick, handleSubmit)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogCard.tsx` around lines 87 - 91, Extract the
inline anonymous onClick handler into a named function called handleCardClick
that accepts the event, calls e.preventDefault(), and then invokes
onCardClick?.(post.slug); replace onClick={() => ...} with
onClick={handleCardClick}. Declare handleCardClick inside the same component
(preferably wrapped with useCallback to avoid recreating the function) and type
its parameter as a React.MouseEvent<HTMLButtonElement> if using TypeScript.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@content/blog/welcome-to-boundless.mdx`:
- Around line 6-8: The frontmatter uses an external placeholder avatar in
author.image which introduces a runtime dependency and privacy/CSP risk; replace
the external URL (https://i.pravatar.cc/150?img=1) with a real, self-hosted
asset or a repository-relative image path (or remove the image field) in the
welcome-to-boundless.mdx frontmatter so author.image points to a locally hosted
file and author.name remains unchanged.

In `@lib/mdx.ts`:
- Around line 61-64: The comparator in posts.sort uses new Date(...).getTime()
which can be NaN for empty/malformed publishedAt values, producing undefined
ordering; change the comparator to normalize each timestamp (e.g., compute tA =
new Date(a.publishedAt).getTime(); if Number.isNaN(tA) set tA to a stable
fallback like 0 or Date.MIN_VALUE-equivalent, do the same for tB) and then
return tB - tA so posts with missing/invalid publishedAt sort consistently (use
the posts.sort call and publishedAt property names to locate and update the
comparator).

---

Outside diff comments:
In `@components/landing-page/blog/BlogCard.tsx`:
- Around line 62-63: BlogCard currently renders the raw ISO string
post.publishedAt; update the component (BlogCard) to parse and format
post.publishedAt before rendering (e.g., using Date constructor or a date
library/Intl.DateTimeFormat) and replace the direct {post.publishedAt} output
with the formatted string; ensure you handle missing or invalid dates (fallback
string or omit) and consider extracting the formatting into a small helper like
formatDate to keep the JSX clean.

---

Duplicate comments:
In `@lib/mdx.ts`:
- Around line 44-45: Filter and sanitize data.categories and data.tags to
include only string elements before assigning them (apply same change in
parseFrontmatter and getBlogPostBySlug); the current code accepts any array and
can break callers that call string methods like tag.toLowerCase() or
tags.includes(t). Replace the direct assignment of data.categories/data.tags
with a guarded expression that checks Array.isArray(...) and then filters to
keep only typeof item === 'string' (optionally trimming each string) so
downstream uses of categories/tags are safe.

---

Nitpick comments:
In `@app/`(landing)/blog/[slug]/page.tsx:
- Around line 50-61: The BlogPostPage arrow function lacks an explicit return
type; update its signature to include a Promise return type (e.g., change "const
BlogPostPage = async ({ params }: BlogPostPageProps) =>" to "const BlogPostPage
= async ({ params }: BlogPostPageProps): Promise<JSX.Element> =>") so the async
server component's return type is explicit; reference the BlogPostPage
identifier and the BlogPostPageProps type when making this change.
- Around line 11-12: The generateStaticParams function is marked async but calls
the synchronous getAllBlogPosts(), so remove the unnecessary async keyword from
generateStaticParams (and any associated await usage or unnecessary Promise
handling) so it returns the mapped slug objects synchronously; locate the
generateStaticParams declaration and change it to a plain function that returns
getAllBlogPosts().map(post => ({ slug: post.slug })).
- Line 19: Wrap the module-level function getBlogPostBySlug with React's cache()
so calls from generateMetadata and BlogPostPage share a single cached result and
avoid duplicate compileMDX/file I/O; update the exported getBlogPostBySlug
definition in the mdx/lib file to be exported as cache(async (slug: string) => {
... }) while keeping the same signature and internal logic so generateMetadata
and BlogPostPage continue calling getBlogPostBySlug unchanged.

In `@components/landing-page/blog/BlogCard.tsx`:
- Around line 87-91: Extract the inline anonymous onClick handler into a named
function called handleCardClick that accepts the event, calls
e.preventDefault(), and then invokes onCardClick?.(post.slug); replace
onClick={() => ...} with onClick={handleCardClick}. Declare handleCardClick
inside the same component (preferably wrapped with useCallback to avoid
recreating the function) and type its parameter as a
React.MouseEvent<HTMLButtonElement> if using TypeScript.

In `@lib/mdx.ts`:
- Around line 51-65: Convert the exported function declarations to const arrow
functions with explicit TypeScript return and parameter types: replace the
`function` declaration for getAllBlogPosts with `const getAllBlogPosts: () =>
MdxBlogPost[] = () => { ... }`, and do the same for getBlogPostBySlug and
getRelatedPosts (e.g., include parameter types like `slug: string` and explicit
return types). Keep existing logic, imports, and exported names identical, and
ensure the functions remain exported (export const ...) so typings and linting
conform to the project's guideline.
- Around line 99-119: getRelatedPosts currently calls getAllBlogPosts() again
causing all .mdx files to be reread; fix by avoiding duplicate I/O either by
changing getRelatedPosts to accept the precomputed posts array (e.g., add an
overload or new signature like getRelatedPosts(currentSlug, tags, categories,
posts, limit)) or by memoizing getAllBlogPosts with a module-level cache
(introduce postsCache: MdxBlogPost[] | null and return cached posts from
getAllBlogPosts when set), and update callers (e.g., getBlogPostBySlug consumer)
to pass the cached list or rely on the memoized getAllBlogPosts to prevent
double-reading.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bae3707 and 9b00682.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • app/(landing)/blog/[slug]/page.tsx
  • components/landing-page/blog/BlogCard.tsx
  • content/blog/building-on-stellar.mdx
  • content/blog/milestone-based-funding.mdx
  • content/blog/welcome-to-boundless.mdx
  • lib/mdx.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • content/blog/building-on-stellar.mdx
  • content/blog/milestone-based-funding.mdx

Comment on lines +6 to +8
author:
name: 'Boundless Team'
image: 'https://i.pravatar.cc/150?img=1'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace placeholder avatar with a real or self-hosted image.

https://i.pravatar.cc/150?img=1 is a third-party placeholder service. Relying on it in production content creates a hard external dependency for every page render and may raise privacy/CSP concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/blog/welcome-to-boundless.mdx` around lines 6 - 8, The frontmatter
uses an external placeholder avatar in author.image which introduces a runtime
dependency and privacy/CSP risk; replace the external URL
(https://i.pravatar.cc/150?img=1) with a real, self-hosted asset or a
repository-relative image path (or remove the image field) in the
welcome-to-boundless.mdx frontmatter so author.image points to a locally hosted
file and author.name remains unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/pre-commit-validation.yml:
- Around line 58-62: The skip-merge guard currently uses echo/grep with the
overly-broad pattern "^Merge " which allows non-git merge-looking commits to
bypass validation; update the guard to use bash's [[ =~ ]] test (avoiding a grep
subprocess) and tighten the regex to match canonical git merge subjects (e.g.
^Merge (pull request #[0-9]+|branch .+ into .+)$ or similar canonical forms) so
only real git-generated merge commits are skipped; apply this change where the
commit subject is checked (the variable message and the current if that uses
echo "$message" | grep -qE "^Merge ") and ensure the new condition combines the
empty-message check and the tightened [[ "$message" =~ <new_regex> ]] test to
continue when appropriate.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9b00682 and 2037b19.

📒 Files selected for processing (1)
  • .github/workflows/pre-commit-validation.yml

@0xdevcollins
Copy link
Collaborator

@akintewe please resolve all coderabbit comments so i can merge

@0xdevcollins
Copy link
Collaborator

image Table is not displayed well

@0xdevcollins
Copy link
Collaborator

Your implementation is solid.
But one thing from the issue is yet to be done, wiring custom components (e.g. Mermaid, Badge) into the MDX compiler so they can be used inside posts. Adding a component map to compileMDX in lib/mdx.ts would close that gap.

Ensure that codeblock, mermaid chart and badges can be rendered, alongside the table rendering

@0xdevcollins
Copy link
Collaborator

@akintewe

- Normalize tag/category query values (trim + lowercase) in search route
  to fix case-sensitive exact-match misses
- Hide 'end of posts' message when search/category filters are active
- Harden frontmatter normalization: coerce all fields to string,
  map array elements via String(), and fix NaN-safe date sort comparator
- Tighten merge-commit skip regex to canonical git forms only
  (branch|pull request|remote-tracking branch) in both CI workflows
- Add MdxComponents.tsx with pre/code, table/thead/th/td/tr overrides,
  Badge (from ui/), and Mermaid fallback renderer
- Wire mdxComponents map into compileMDX so code blocks, tables,
  badges and mermaid diagrams render correctly inside MDX posts
@akintewe
Copy link
Contributor Author

@akintewe

noted, working on it

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/landing-page/blog/BlogGrid.tsx (1)

98-130: ⚠️ Potential issue | 🟡 Minor

Remove dead API endpoint code in handleLoadMore.

The call to /api/blog/posts references a removed API route since the blog now uses file-based MDX. The function should use client-side pagination by directly incrementing visiblePosts, which is the fallback behavior already present in the code. The current implementation causes unnecessary failed network requests on every "Load More" click.

Simplify handleLoadMore to directly increment visiblePosts without attempting to fetch from the removed API endpoint. Update the dependency array to [] since the simplified logic doesn't depend on isLoading or hasMorePosts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogGrid.tsx` around lines 98 - 130, The
handleLoadMore function should be simplified to remove the dead
`/api/blog/posts` fetch: delete the network request, response handling,
currentPage/hasMorePosts updates and catch/fallback branches, and instead just
increment visiblePosts (via setVisiblePosts(prev => prev + 12)) and clear
isLoading as needed; update the dependency array for useCallback to [] since the
new logic no longer depends on isLoading, hasMorePosts or currentPage; ensure
you still guard against concurrent clicks by keeping and toggling isLoading with
setIsLoading at the start and end of the callback.
🧹 Nitpick comments (3)
components/landing-page/blog/BlogGrid.tsx (1)

146-152: Consider using underscore prefix for unused parameter.

void slug; works but prefixing with underscore (_slug) is more idiomatic for intentionally unused parameters.

♻️ Proposed refactor
-  const handleCardClick = useCallback((slug: string) => {
-    void slug;
+  const handleCardClick = useCallback((_slug: string) => {
     setIsNavigating(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/BlogGrid.tsx` around lines 146 - 152, The
handleCardClick callback currently silences the unused slug parameter with "void
slug;"—replace this by renaming the parameter to _slug (or _slug: string) in the
handleCardClick signature to make the unused intention idiomatic, and update any
internal references accordingly; leave the setIsNavigating logic and timeout
unchanged.
lib/mdx.ts (1)

73-108: Consider extracting shared frontmatter normalization logic.

The frontmatter normalization at lines 89–105 duplicates the logic in parseFrontmatter (lines 35–53). Extracting a shared helper would reduce maintenance burden and ensure consistency.

♻️ Proposed refactor
+function normalizePostData(slug: string, data: Record<string, unknown>): MdxBlogPost {
+  return {
+    slug,
+    title: String(data.title ?? ''),
+    excerpt: String(data.excerpt ?? ''),
+    coverImage: String(data.coverImage ?? ''),
+    publishedAt: String(data.publishedAt ?? ''),
+    author: {
+      name: String(data.author?.name ?? ''),
+      image: String(data.author?.image ?? ''),
+    },
+    categories: Array.isArray(data.categories)
+      ? data.categories.map((c: unknown) => String(c))
+      : [],
+    tags: Array.isArray(data.tags)
+      ? data.tags.map((t: unknown) => String(t))
+      : [],
+    readingTime: typeof data.readingTime === 'number' ? data.readingTime : 0,
+    isFeatured: data.isFeatured === true,
+  };
+}
+
 function parseFrontmatter(slug: string): MdxBlogPost {
   const filePath = path.join(BLOG_DIR, `${slug}.mdx`);
   const raw = fs.readFileSync(filePath, 'utf8');
   const { data } = matter(raw);
-
-  return {
-    slug,
-    title: String(data.title ?? ''),
-    // ... rest of normalization
-  };
+  return normalizePostData(slug, data);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mdx.ts` around lines 73 - 108, getBlogPostBySlug duplicates the
frontmatter normalization logic already implemented in parseFrontmatter; extract
a shared helper (e.g., normalizeFrontmatter or normalizeBlogMeta) that accepts
the raw frontmatter object and returns the normalized shape (title, excerpt,
coverImage, publishedAt, author, categories, tags, readingTime, isFeatured) and
then call that helper from both getBlogPostBySlug and parseFrontmatter so both
use the same normalization routine (update function names: getBlogPostBySlug,
parseFrontmatter, normalizeFrontmatter to locate usage).
components/landing-page/blog/MdxComponents.tsx (1)

93-109: Mermaid fallback does not render actual diagrams.

The current implementation displays raw diagram source text rather than rendered diagrams. The comment at lines 95–96 documents this limitation. If actual Mermaid rendering is needed (as mentioned in PR comments), consider adding a client component wrapper that imports mermaid.js.

Is the raw text fallback sufficient for the initial release, or should actual Mermaid diagram rendering be implemented before merge?

Do you want me to help implement a client-side Mermaid renderer component?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/MdxComponents.tsx` around lines 93 - 109, The
Mermaid component currently only renders raw diagram source (function Mermaid)
instead of actual diagrams; replace or augment it with a client-only component
that imports mermaid, uses 'use client', and renders into a ref inside
useEffect: initialize mermaid with mermaid.initialize, call mermaid.render or
mermaid.parse+render to convert the children string into SVG/HTML, insert that
output into a container ref, and catch/log errors; keep the existing
preformatted fallback for SSR by conditionally rendering the client renderer
only on the client (e.g., export a server-safe wrapper that shows the <pre>
fallback and loads the client MermaidRenderer component on the client). Ensure
you reference the Mermaid function and its children prop, sanitize/escape inputs
if needed, and preserve styling and accessibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/landing-page/blog/StreamingBlogGrid.tsx`:
- Around line 64-70: The date comparator in StreamingBlogGrid.tsx can produce
NaN when publishedAt is missing/malformed; update the sort block that builds
filtered (the anonymous comparator inside filtered.sort) to parse each date
defensively—convert new Date(a.publishedAt).getTime() and new
Date(b.publishedAt).getTime() into numbers that fall back to 0 when NaN (e.g.,
use a || 0 or Number.isNaN checks) so the comparator always returns a valid
numeric difference and produces deterministic ordering for invalid dates.

---

Outside diff comments:
In `@components/landing-page/blog/BlogGrid.tsx`:
- Around line 98-130: The handleLoadMore function should be simplified to remove
the dead `/api/blog/posts` fetch: delete the network request, response handling,
currentPage/hasMorePosts updates and catch/fallback branches, and instead just
increment visiblePosts (via setVisiblePosts(prev => prev + 12)) and clear
isLoading as needed; update the dependency array for useCallback to [] since the
new logic no longer depends on isLoading, hasMorePosts or currentPage; ensure
you still guard against concurrent clicks by keeping and toggling isLoading with
setIsLoading at the start and end of the callback.

---

Nitpick comments:
In `@components/landing-page/blog/BlogGrid.tsx`:
- Around line 146-152: The handleCardClick callback currently silences the
unused slug parameter with "void slug;"—replace this by renaming the parameter
to _slug (or _slug: string) in the handleCardClick signature to make the unused
intention idiomatic, and update any internal references accordingly; leave the
setIsNavigating logic and timeout unchanged.

In `@components/landing-page/blog/MdxComponents.tsx`:
- Around line 93-109: The Mermaid component currently only renders raw diagram
source (function Mermaid) instead of actual diagrams; replace or augment it with
a client-only component that imports mermaid, uses 'use client', and renders
into a ref inside useEffect: initialize mermaid with mermaid.initialize, call
mermaid.render or mermaid.parse+render to convert the children string into
SVG/HTML, insert that output into a container ref, and catch/log errors; keep
the existing preformatted fallback for SSR by conditionally rendering the client
renderer only on the client (e.g., export a server-safe wrapper that shows the
<pre> fallback and loads the client MermaidRenderer component on the client).
Ensure you reference the Mermaid function and its children prop, sanitize/escape
inputs if needed, and preserve styling and accessibility.

In `@lib/mdx.ts`:
- Around line 73-108: getBlogPostBySlug duplicates the frontmatter normalization
logic already implemented in parseFrontmatter; extract a shared helper (e.g.,
normalizeFrontmatter or normalizeBlogMeta) that accepts the raw frontmatter
object and returns the normalized shape (title, excerpt, coverImage,
publishedAt, author, categories, tags, readingTime, isFeatured) and then call
that helper from both getBlogPostBySlug and parseFrontmatter so both use the
same normalization routine (update function names: getBlogPostBySlug,
parseFrontmatter, normalizeFrontmatter to locate usage).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2037b19 and 879a93b.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • .github/workflows/pre-commit-validation.yml
  • app/api/blog/search/route.ts
  • components/landing-page/blog/BlogGrid.tsx
  • components/landing-page/blog/MdxComponents.tsx
  • components/landing-page/blog/StreamingBlogGrid.tsx
  • lib/mdx.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml

Comment on lines 64 to 70
if (sortOrder) {
filtered = [...filtered].sort((a, b) => {
const dateA = new Date(a.coverImage).getTime();
const dateB = new Date(b.createdAt).getTime();
const dateA = new Date(a.publishedAt).getTime();
const dateB = new Date(b.publishedAt).getTime();
return sortOrder === 'Latest' ? dateB - dateA : dateA - dateB;
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sort comparator may produce undefined ordering for invalid dates.

Unlike lib/mdx.ts which handles NaN dates with a fallback to 0, this comparator can return NaN if publishedAt is empty or malformed, leading to unpredictable sort order.

🛡️ Proposed fix for consistent NaN handling
     if (sortOrder) {
       filtered = [...filtered].sort((a, b) => {
-        const dateA = new Date(a.publishedAt).getTime();
-        const dateB = new Date(b.publishedAt).getTime();
-        return sortOrder === 'Latest' ? dateB - dateA : dateA - dateB;
+        const ta = a.publishedAt ? new Date(a.publishedAt).getTime() : 0;
+        const tb = b.publishedAt ? new Date(b.publishedAt).getTime() : 0;
+        const dateA = isNaN(ta) ? 0 : ta;
+        const dateB = isNaN(tb) ? 0 : tb;
+        return sortOrder === 'Latest' ? dateB - dateA : dateA - dateB;
       });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/blog/StreamingBlogGrid.tsx` around lines 64 - 70, The
date comparator in StreamingBlogGrid.tsx can produce NaN when publishedAt is
missing/malformed; update the sort block that builds filtered (the anonymous
comparator inside filtered.sort) to parse each date defensively—convert new
Date(a.publishedAt).getTime() and new Date(b.publishedAt).getTime() into numbers
that fall back to 0 when NaN (e.g., use a || 0 or Number.isNaN checks) so the
comparator always returns a valid numeric difference and produces deterministic
ordering for invalid dates.

GitHub's merge queue generates 'Merge <hash> into <hash>' messages
which were not matched by the previous branch/PR/remote-tracking pattern.
@akintewe
Copy link
Contributor Author

@akintewe

i have made the fixes, please check

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

161-165: Consider extending the merge commit pattern for completeness.

The current regex misses some git-generated merge message formats:

  • Merge commit 'abc123' — explicit commit merge
  • Merge branches 'a' and 'b' — octopus merge (plural)
  • Merge tag 'v1.0' — tag merge
Suggested fix to cover additional merge formats
             # Skip git-generated merge commits
-            if echo "$message" | grep -qE "^Merge (branch|pull request|remote-tracking branch|[0-9a-f]{7,40}) "; then
+            if echo "$message" | grep -qE "^Merge (branch(es)?|pull request|remote-tracking branch|commit|tag|[0-9a-f]{7,40}) "; then
               continue
             fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 161 - 165, Update the grep pattern
that skips git-generated merge commits (the line using echo "$message" | grep
-qE "^Merge ...") to also match "Merge commit '...'", "Merge branches 'a' and
'b'" (and the singular "Merge branch 'a'"), and "Merge tag 'v1.0'". Replace the
current regex with a more comprehensive one that includes alternatives for
commit, tag, and branches patterns (for example: ^Merge (commit '[^']+'|tags?
'[^']+'|branches? '[^']+'( and '[^']+')?|branch|pull request|remote-tracking
branch|[0-9a-f]{7,40}) ) so the grep -qE call inside that if block will
correctly skip these additional git-generated merge message formats.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 161-165: Update the grep pattern that skips git-generated merge
commits (the line using echo "$message" | grep -qE "^Merge ...") to also match
"Merge commit '...'", "Merge branches 'a' and 'b'" (and the singular "Merge
branch 'a'"), and "Merge tag 'v1.0'". Replace the current regex with a more
comprehensive one that includes alternatives for commit, tag, and branches
patterns (for example: ^Merge (commit '[^']+'|tags? '[^']+'|branches? '[^']+'(
and '[^']+')?|branch|pull request|remote-tracking branch|[0-9a-f]{7,40}) ) so
the grep -qE call inside that if block will correctly skip these additional
git-generated merge message formats.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 879a93b and 6ebd2fa.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • .github/workflows/pre-commit-validation.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/pre-commit-validation.yml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

convert blog from API to full MDX

2 participants