Skip to content
Closed
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
184 changes: 62 additions & 122 deletions lib/markdown-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,128 +1,68 @@
interface MarkdownRendererProps {
content: string
}
import ReactMarkdown from "react-markdown"
import type { Components } from "react-markdown"
import matter from "gray-matter"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the current double-parse path.
sed -n '18,36p' 'lib/blog-server.ts'
sed -n '199,201p' 'app/a/[slug]/BlogPostClient.tsx'
sed -n '60,65p' 'lib/markdown-renderer.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 1037


🏁 Script executed:

#!/bin/bash
# Check if markdown-renderer.tsx is a client component and inspect full file
head -20 'lib/markdown-renderer.tsx'
echo "---"
wc -l 'lib/markdown-renderer.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 1290


🏁 Script executed:

#!/bin/bash
# Check if gray-matter is appropriate for client-side use
grep -n "gray-matter\|matter" 'lib/markdown-renderer.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Verify blog-server.ts is server-side and check the return type
head -5 'lib/blog-server.ts'
grep -A 5 "export.*BlogPost" 'lib/blog-server.ts' | head -15

Repository: StabilityNexus/StableViewpoints

Length of output: 617


🏁 Script executed:

#!/bin/bash
# Check complete markdown-renderer.tsx file for "use client" and full structure
cat -n 'lib/markdown-renderer.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 3233


🏁 Script executed:

#!/bin/bash
# Check how MarkdownRenderer is imported/used in BlogPostClient
grep -B5 -A5 "MarkdownRenderer" 'app/a/\[slug\]/BlogPostClient.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 141


🏁 Script executed:

#!/bin/bash
# Verify if gray-matter has 'fs' dependency that would be problematic in client
grep -r "require.*fs\|import.*fs" node_modules/gray-matter/index.js 2>/dev/null | head -5 || echo "Checking gray-matter package info..."
cat package.json | grep -A5 "gray-matter"

Repository: StabilityNexus/StableViewpoints

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Find the actual BlogPostClient file
find . -name "BlogPostClient.tsx" -o -name "*BlogPost*.tsx" | grep -E "client|Client"

Repository: StabilityNexus/StableViewpoints

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check if there's a "use client" directive in markdown-renderer.tsx (which there isn't based on cat output)
# and verify if this file is actually used only on server or in client context
grep -r "MarkdownRenderer" 'app/' --include="*.tsx" --include="*.ts"

Repository: StabilityNexus/StableViewpoints

Length of output: 255


🏁 Script executed:

#!/bin/bash
# Check gray-matter's actual dependencies to see if it can run in browser
cat package.json | jq '.dependencies, .devDependencies' | grep -A10 "gray"

Repository: StabilityNexus/StableViewpoints

Length of output: 340


🏁 Script executed:

#!/bin/bash
# Check BlogPostClient.tsx for "use client" directive and how it uses MarkdownRenderer
head -30 'app/a/[slug]/BlogPostClient.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 1294


🏁 Script executed:

#!/bin/bash
# Check for "use client" specifically
grep -n "use client" 'app/a/[slug]/BlogPostClient.tsx' 'lib/markdown-renderer.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Verify gray-matter is a Node.js package and check its main entry
cat node_modules/gray-matter/package.json 2>/dev/null | jq '.main, .browser, .exports' || echo "Checking gray-matter compatibility..."
npm list gray-matter 2>/dev/null | head -3

Repository: StabilityNexus/StableViewpoints

Length of output: 135


🏁 Script executed:

#!/bin/bash
# Check gray-matter's browser compatibility and dependencies
cat node_modules/gray-matter/package.json 2>/dev/null | head -30 || echo "Package not found in node_modules"
# Try to find what gray-matter depends on
find node_modules/gray-matter -name "*.json" -type f 2>/dev/null | head -1 | xargs cat 2>/dev/null | grep -E '"dependencies"|"main"|"browser"' | head -10

Repository: StabilityNexus/StableViewpoints

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check if markdown-renderer.tsx should have "use client" or if there's another version
find . -name "markdown-renderer*" -type f
# Also check if lib files are typically server or client in this project
head -5 lib/*.tsx lib/*.ts 2>/dev/null | grep -E "^==>|use client"

Repository: StabilityNexus/StableViewpoints

Length of output: 228


🏁 Script executed:

#!/bin/bash
# Check the complete BlogPostClient to understand the data flow
cat -n 'app/a/[slug]/BlogPostClient.tsx' | head -50

Repository: StabilityNexus/StableViewpoints

Length of output: 2349


🏁 Script executed:

#!/bin/bash
# Check if there's a layout or page file that calls BlogPostClient
find 'app/a/[slug]' -name "*.tsx" -o -name "*.ts"

Repository: StabilityNexus/StableViewpoints

Length of output: 128


🏁 Script executed:

#!/bin/bash
# Verify the BlogPost type to understand what post.content contains
grep -A10 "interface BlogPost\|type BlogPost" lib/blog.ts lib/blog-server.ts

Repository: StabilityNexus/StableViewpoints

Length of output: 750


🏁 Script executed:

#!/bin/bash
# Check page.tsx to understand the complete data flow
cat 'app/a/[slug]/page.tsx'

Repository: StabilityNexus/StableViewpoints

Length of output: 708


🏁 Script executed:

#!/bin/bash
# Check if gray-matter has any browser/client-side support or dependencies
python3 << 'EOF'
import json
try:
    with open('node_modules/gray-matter/package.json', 'r') as f:
        data = json.load(f)
        print("Main:", data.get('main'))
        print("Browser:", data.get('browser'))
        print("Dependencies:", json.dumps(data.get('dependencies', {}), indent=2))
except Exception as e:
    print(f"Error: {e}")
EOF

Repository: StabilityNexus/StableViewpoints

Length of output: 158


Remove the redundant gray-matter parse and import.

Content is already parsed in lib/blog-server.ts:18-36 by getPostBySlugServer(), which extracts and returns only the body. Parsing again in the renderer violates NextJS client-rendering best practices since gray-matter is a Node.js library, adds unnecessary render-time work, and can incorrectly re-parse a legitimate leading --- in the article body.

Suggested fix
-import matter from "gray-matter"
@@
 export function MarkdownRenderer({ content }: MarkdownRendererProps) {
-  const { content: body } = matter(content)
-
   return (
     <div className="prose prose-lg max-w-none">
-      <ReactMarkdown components={components}>{body}</ReactMarkdown>
+      <ReactMarkdown components={components}>{content}</ReactMarkdown>
     </div>
   )
 }
📝 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.

Suggested change
import matter from "gray-matter"
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
return (
<div className="prose prose-lg max-w-none">
<ReactMarkdown components={components}>{content}</ReactMarkdown>
</div>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/markdown-renderer.tsx` at line 3, Remove the gray-matter import and any
re-parsing in lib/markdown-renderer.tsx: delete the import statement for
"gray-matter" and remove calls to matter() or grayMatter.parse() inside the
Markdown renderer component (e.g., the component/function that consumes post
content). Ensure the renderer accepts the already-extracted body (the string
returned by getPostBySlugServer()) and use that directly for rendering; update
the component's prop/interface to expect body: string if necessary and remove
any logic that attempted to split/front-matter-process the input.


// Conditionally set BASE_PATH based on environment
const BASE_PATH = process.env.NODE_ENV === "production" ? "/StableViewpoints" : ""

export function MarkdownRenderer({ content }: MarkdownRendererProps) {
// Enhanced markdown to HTML conversion
const renderMarkdown = (text: string) => {
let html = text

// Headers
html = html.replace(/^### (.*$)/gim, '<h3 class="text-xl font-medium text-gray-800 mt-5 mb-2">$1</h3>')
html = html.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-semibold text-gray-800 mt-6 mb-3">$1</h2>')
html = html.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold text-gray-900 mt-8 mb-4">$1</h1>')

// Bold and italic
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>")

// Code blocks
html = html.replace(
/```([\s\S]*?)```/g,
'<pre class="bg-gray-100 p-4 rounded-lg overflow-x-auto my-4"><code class="text-sm">$1</code></pre>'
)

// Inline code
html = html.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1 py-0.5 rounded text-sm">$1</code>')

// Images - handle base path for GitHub Pages
html = html.replace(
/!\[(.*?)\]\((.*?)\)/gim,
(match, alt, src) => {
// Add base path to image src if it starts with /
const imageSrc = src.startsWith('/') ? `${BASE_PATH}${src}` : src
return `<div class="my-8"><img src="${imageSrc}" alt="${alt}" class="rounded-lg w-full h-auto max-w-2xl mx-auto shadow-lg" /></div>`
}
)

// Links
html = html.replace(
/\[(.*?)\]\((.*?)\)/gim,
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-[#228B22] hover:text-[#3E921E] underline">$1</a>',
)

// Blockquotes
html = html.replace(
/^> (.*$)/gim,
'<blockquote class="border-l-4 border-[#FFBF00] pl-4 italic text-gray-600 my-6 bg-yellow-50 py-2">$1</blockquote>',
const components: Components = {
h1: ({ children }) => <h1 className="text-3xl font-bold text-gray-900 mt-8 mb-4">{children}</h1>,
h2: ({ children }) => <h2 className="text-2xl font-semibold text-gray-800 mt-6 mb-3">{children}</h2>,
h3: ({ children }) => <h3 className="text-xl font-medium text-gray-800 mt-5 mb-2">{children}</h3>,
h4: ({ children }) => <h4 className="text-lg font-medium text-gray-800 mt-4 mb-2">{children}</h4>,

p: ({ children }) => <p className="text-gray-700 leading-relaxed mb-4">{children}</p>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find posts that will currently hit the invalid paragraph/image nesting path.
rg -n --glob 'public/articles/**/*.md' '^\s*!\[[^]]*\]\([^)]+\)\s*$|^\s*\[!\[[^]]*\]\([^)]+\)\]\([^)]+\)\s*$'

Repository: StabilityNexus/StableViewpoints

Length of output: 1784


🏁 Script executed:

cat -n lib/markdown-renderer.tsx

Repository: StabilityNexus/StableViewpoints

Length of output: 3233


Remove the <div> wrapper from the image renderer to fix invalid HTML nesting.

When markdown contains a standalone image (like ![alt](image.jpg)), react-markdown renders it as a paragraph child. With the current p override always wrapping in <p> and the img override wrapping in <div>, this creates invalid <p><div>…</div></p> markup. This violates HTML semantics since block elements cannot directly contain other block elements, and can trigger DOM nesting warnings and layout issues in Next.js.

Move the spacing to the <img> element itself with the block display class, or special-case image-only paragraphs. Verify this affects your markdown articles:

Suggested fix
   img: ({ src, alt }) => {
     const resolvedSrc = typeof src === "string" && src.startsWith("/") ? `${BASE_PATH}${src}` : src
-    return (
-      <div className="my-8">
-        {/* eslint-disable-next-line `@next/next/no-img-element` */}
-        <img src={resolvedSrc} alt={alt ?? ""} className="rounded-lg w-full h-auto max-w-2xl mx-auto shadow-lg" />
-      </div>
-    )
+    // eslint-disable-next-line `@next/next/no-img-element`
+    return <img src={resolvedSrc} alt={alt ?? ""} className="my-8 block w-full h-auto max-w-2xl mx-auto rounded-lg shadow-lg" />
   },

16 standalone images in your article files would be affected by this issue.

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

In `@lib/markdown-renderer.tsx` at line 13, The <p> override (p: ({ children }) =>
<p className="text-gray-700 leading-relaxed mb-4">{children}</p>) together with
the image override creates invalid <p><div>…</div></p> nesting; remove the extra
<div> wrapper from the image renderer (the img override) and instead apply
spacing/layout classes directly to the <img> (e.g., add block and margin
classes) or detect and special-case image-only paragraphs so the image renders
as a block-level element without being wrapped in a <div>, ensuring the p
override remains a plain <p> and the img renderer uses only inline/block-safe
markup.

strong: ({ children }) => <strong className="font-semibold text-gray-900">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,

ul: ({ children }) => <ul className="list-disc list-inside mb-4 text-gray-700 space-y-1">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal list-inside mb-4 text-gray-700 space-y-1">{children}</ol>,
li: ({ children }) => <li className="mb-1">{children}</li>,

blockquote: ({ children }) => (
<blockquote className="border-l-4 border-[#FFBF00] pl-4 italic text-gray-600 my-6 bg-yellow-50 py-2">
{children}
</blockquote>
),

hr: () => <hr className="my-8 border-gray-300" />,

code: ({ className, children, ...props }) => {
const isBlock = Boolean(className)
if (isBlock) return <code className={className}>{children}</code>
Comment thread
bored-arvi marked this conversation as resolved.
return <code className="bg-gray-100 px-1 py-0.5 rounded text-sm" {...props}>{children}</code>
},
Comment on lines +29 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find fenced code blocks without an info string.
rg -n --glob 'public/articles/**/*.md' '^(```|~~~)[[:space:]]*$'

Repository: StabilityNexus/StableViewpoints

Length of output: 5052


Fix block code detection to handle unlabeled fenced blocks.

Fenced code blocks without a language specifier (e.g., plain ```) don't receive a className prop in react-markdown, causing them to render with inline styling instead of block styling. The current Boolean(className) check misses these cases. Add a pre parent or wrapper check, or rely on another signal to distinguish block-level code from inline code.

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

In `@lib/markdown-renderer.tsx` around lines 29 - 33, The code renderer in
lib/markdown-renderer.tsx currently detects block code via Boolean(className),
which fails for unlabeled fenced blocks; update the code renderer (the code:
component) to accept the renderer props including the inline boolean (e.g., ({
inline, className, children, ...props })) and use the inline flag (render block
when inline === false) rather than Boolean(className), falling back to className
when appropriate so unlabeled fenced blocks render with block styling.


pre: ({ children }) => (
<pre className="bg-gray-100 p-4 rounded-lg overflow-x-auto my-4 text-sm">{children}</pre>
),

a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-[#228B22] hover:text-[#3E921E] underline">
{children}
</a>
),

img: ({ src, alt }) => {
const resolvedSrc = typeof src === "string" && src.startsWith("/") ? `${BASE_PATH}${src}` : src
return (
<div className="my-8">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={resolvedSrc} alt={alt ?? ""} className="rounded-lg w-full h-auto max-w-2xl mx-auto shadow-lg" />
</div>
)
},
}

// Horizontal rules
html = html.replace(/^---$/gm, '<hr class="my-8 border-gray-300" />')

// Lists - improved list handling
const lines = html.split("\n")
let inList = false
let inOrderedList = false
const processedLines: string[] = []

for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()

if (trimmed.startsWith("- ")) {
if (!inList) {
processedLines.push('<ul class="list-disc list-inside mb-4 text-gray-700 space-y-1">')
inList = true
}
processedLines.push(`<li class="mb-1">${trimmed.substring(2)}</li>`)
} else if (trimmed.match(/^\d+\. /)) {
if (!inOrderedList) {
processedLines.push('<ol class="list-decimal list-inside mb-4 text-gray-700 space-y-1">')
inOrderedList = true
}
processedLines.push(`<li class="mb-1">${trimmed.replace(/^\d+\. /, '')}</li>`)
} else {
if (inList) {
processedLines.push("</ul>")
inList = false
}
if (inOrderedList) {
processedLines.push("</ol>")
inOrderedList = false
}
processedLines.push(line)
}
}

if (inList) {
processedLines.push("</ul>")
}
if (inOrderedList) {
processedLines.push("</ol>")
}

html = processedLines.join("\n")

// Paragraphs
const finalLines = html.split("\n")
const finalProcessedLines = finalLines.map((line) => {
const trimmed = line.trim()
if (trimmed === "") return ""
if (
trimmed.startsWith("<h") ||
trimmed.startsWith("<blockquote") ||
trimmed.startsWith("<ul") ||
trimmed.startsWith("</ul>") ||
trimmed.startsWith("<ol") ||
trimmed.startsWith("</ol>") ||
trimmed.startsWith("<li") ||
trimmed.startsWith("<div") ||
trimmed.startsWith("<img") ||
trimmed.startsWith("<pre") ||
trimmed.startsWith("<hr") ||
trimmed.startsWith("<code")
) {
return trimmed
}
return `<p class="text-gray-700 leading-relaxed mb-4">${trimmed}</p>`
})

return finalProcessedLines.join("\n")
}

return <div className="prose prose-lg max-w-none" dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }} />
interface MarkdownRendererProps {
content: string
}

export function MarkdownRenderer({ content }: MarkdownRendererProps) {
const { content: body } = matter(content)

Comment thread
bored-arvi marked this conversation as resolved.
return (
<div className="prose prose-lg max-w-none">
<ReactMarkdown components={components}>{body}</ReactMarkdown>
</div>
Comment thread
bored-arvi marked this conversation as resolved.
)
}
49 changes: 38 additions & 11 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"react-dom": "^19",
"serve": "^14.2.4",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"react-markdown": "^9.0.1"
Comment thread
bored-arvi marked this conversation as resolved.
},
"devDependencies": {
"@types/node": "^22",
Expand Down
Loading