fix(content): replace hand-rolled MDX renderer with marked#28
Conversation
Resolves the second P0 in docs/CONTENT-AUDIT-2026-07-16.md: the lesson renderer couldn't display tables, ordered lists, nested lists, links, or images used throughout content/curriculum/modules/ (28 files use tables, 23 use ordered lists, one has 3-level nested lists in a decision flowchart). Multi-line blockquote callouts also rendered as several broken adjacent <blockquote> tags instead of one block with its nested list intact. Swaps the regex-based parser for `marked` (CommonMark + GFM, zero runtime deps) instead of continuing to patch the hand-rolled one by hand. Raw HTML (block and inline) is escaped rather than passed through, and link/image URLs are restricted to http(s), mailto, and relative/anchor paths — everything else is dropped to its text content — preserving the "no raw HTML" security posture of the original renderer. Added CSS for table/th/td/a/img, none of which existed before since the old renderer never emitted them. Verified against a running instance (seeded Postgres + real curriculum import) that lessons with tables, multi-line callouts, and nested flowcharts render correctly. Video embeds remain out of scope, per the audit's own note not to start the video program until a lesson can render and track a video correctly — no current lesson content uses them.
📝 WalkthroughWalkthroughThe lesson Markdown renderer now delegates parsing to ChangesLesson rendering
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/__tests__/mdx.test.ts`:
- Around line 6-33: Move the `mdx.test.ts` suite from the `__tests__` directory
to sit alongside `mdx.ts` in `src/lib`, preserving all existing tests and
behavior.
- Around line 127-132: Remove the emoji literals from the comment and input
fixture in the multi-line blockquote test, while preserving the callout text
structure and test behavior. Update the related reference in the test
description/comment as needed without changing the parsing scenario.
In `@src/lib/mdx.ts`:
- Around line 2-13: Replace the em dashes in the module-level comments of the
MDX renderer with permitted punctuation, preserving the existing descriptions
and meaning.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3864431e-5899-4e66-a74a-e7725accc27d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (4)
package.jsonsrc/app/(dashboard)/courses/[courseSlug]/lessons/[lessonSlug]/lesson.module.csssrc/lib/__tests__/mdx.test.tssrc/lib/mdx.ts
| expect(renderLesson('Hello world').trim()).toBe('<p>Hello world</p>'); | ||
| }); | ||
|
|
||
| it('renders multiple paragraphs separated by blank lines', () => { | ||
| const input = 'First paragraph.\n\nSecond paragraph.'; | ||
| const result = renderLesson(input); | ||
| const result = renderLesson('First paragraph.\n\nSecond paragraph.'); | ||
| expect(result).toContain('<p>First paragraph.</p>'); | ||
| expect(result).toContain('<p>Second paragraph.</p>'); | ||
| }); | ||
|
|
||
| it('renders headings h1 through h4', () => { | ||
| expect(renderLesson('# Title')).toBe('<h1>Title</h1>'); | ||
| expect(renderLesson('## Section')).toBe('<h2>Section</h2>'); | ||
| expect(renderLesson('### Subsection')).toBe('<h3>Subsection</h3>'); | ||
| expect(renderLesson('#### Detail')).toBe('<h4>Detail</h4>'); | ||
| expect(renderLesson('# Title').trim()).toBe('<h1>Title</h1>'); | ||
| expect(renderLesson('## Section').trim()).toBe('<h2>Section</h2>'); | ||
| expect(renderLesson('### Subsection').trim()).toBe('<h3>Subsection</h3>'); | ||
| expect(renderLesson('#### Detail').trim()).toBe('<h4>Detail</h4>'); | ||
| }); | ||
|
|
||
| it('renders bold text', () => { | ||
| expect(renderLesson('This is **bold** text')).toBe('<p>This is <strong>bold</strong> text</p>'); | ||
| expect(renderLesson('This is **bold** text')).toContain('This is <strong>bold</strong> text'); | ||
| }); | ||
|
|
||
| it('renders italic text', () => { | ||
| expect(renderLesson('This is *italic* text')).toBe('<p>This is <em>italic</em> text</p>'); | ||
| expect(renderLesson('This is *italic* text')).toContain('This is <em>italic</em> text'); | ||
| }); | ||
|
|
||
| it('renders inline code', () => { | ||
| expect(renderLesson('Use the `renderLesson` function')).toBe('<p>Use the <code>renderLesson</code> function</p>'); | ||
| expect(renderLesson('Use the `renderLesson` function')).toContain( | ||
| 'Use the <code>renderLesson</code> function' | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Co-locate this suite with mdx.ts.
Move src/lib/__tests__/mdx.test.ts to src/lib/mdx.test.ts so the tests sit beside the module they cover. As per coding guidelines, **/*.test.{ts,tsx} tests must be placed next to the code they test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/__tests__/mdx.test.ts` around lines 6 - 33, Move the `mdx.test.ts`
suite from the `__tests__` directory to sit alongside `mdx.ts` in `src/lib`,
preserving all existing tests and behavior.
Source: Coding guidelines
| it('renders multi-line blockquote callouts with a nested list as one block', () => { | ||
| // Matches the "🎯 Analogy" / "📌 Key Takeaway" callout pattern used throughout lessons. | ||
| const input = | ||
| '> 🎯 **Analogy**: Three ways to pay for a taxi:\n' + | ||
| '> - **Fixed Bids** = flat-rate ride share\n' + | ||
| '> - **Dynamic** = surge pricing'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove emoji literals from the test source.
The changed comment and fixture contain emoji characters, which the repository prohibits in code.
Proposed fix
- // Matches the "🎯 Analogy" / "📌 Key Takeaway" callout pattern used throughout lessons.
+ // Covers callout patterns used throughout lessons.
...
- '> 🎯 **Analogy**: Three ways to pay for a taxi:\n' +
+ '> \u{1F3AF} **Analogy**: Three ways to pay for a taxi:\n' +As per coding guidelines, **/*.{ts,tsx,js,jsx} must not use emojis in code.
📝 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.
| it('renders multi-line blockquote callouts with a nested list as one block', () => { | |
| // Matches the "🎯 Analogy" / "📌 Key Takeaway" callout pattern used throughout lessons. | |
| const input = | |
| '> 🎯 **Analogy**: Three ways to pay for a taxi:\n' + | |
| '> - **Fixed Bids** = flat-rate ride share\n' + | |
| '> - **Dynamic** = surge pricing'; | |
| it('renders multi-line blockquote callouts with a nested list as one block', () => { | |
| // Covers callout patterns used throughout lessons. | |
| const input = | |
| '> \u{1F3AF} **Analogy**: Three ways to pay for a taxi:\n' + | |
| '> - **Fixed Bids** = flat-rate ride share\n' + | |
| '> - **Dynamic** = surge pricing'; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/__tests__/mdx.test.ts` around lines 127 - 132, Remove the emoji
literals from the comment and input fixture in the multi-line blockquote test,
while preserving the callout text structure and test behavior. Update the
related reference in the test description/comment as needed without changing the
parsing scenario.
Source: Coding guidelines
| * MDX (markdown subset) -> HTML renderer for lesson content. | ||
| * | ||
| * Handles the markdown subset we use in lessons: | ||
| * - Headings (# ## ### ####) | ||
| * - Paragraphs | ||
| * - Bold/italic (**text**, *text*) | ||
| * - Inline code (`code`) and code blocks (```) | ||
| * - Bullet lists (- item) | ||
| * - Blockquotes (> text) | ||
| * - Horizontal rules (---) | ||
| * Backed by `marked` (CommonMark + GFM) so lessons can use tables, ordered | ||
| * and nested lists, links, and images, none of which the hand-rolled | ||
| * predecessor of this module supported. | ||
| * | ||
| * No JSX components — pure markdown. For interactive components | ||
| * (e.g., drag-drop simulations) we'll need a real MDX setup, but the | ||
| * current AMPH content is markdown-only. | ||
| * No JSX/component support — pure markdown. Interactive components would | ||
| * need a real MDX compile step; current AMPH content is markdown-only. | ||
| * | ||
| * Security: all output is HTML-escaped. No raw HTML passes through. | ||
| * Security: raw HTML (block or inline) is escaped rather than passed | ||
| * through, and link/image URLs are restricted to http(s), mailto, and | ||
| * relative/anchor paths — everything else is dropped to its text content. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace em dashes in source comments.
Lines 8 and 13 introduce em dashes, which are prohibited in TypeScript source.
Proposed fix
- * No JSX/component support — pure markdown. Interactive components would
+ * No JSX/component support. This is pure markdown. Interactive components would
...
- * relative/anchor paths — everything else is dropped to its text content.
+ * relative/anchor paths. Everything else is dropped to its text content.📝 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.
| * MDX (markdown subset) -> HTML renderer for lesson content. | |
| * | |
| * Handles the markdown subset we use in lessons: | |
| * - Headings (# ## ### ####) | |
| * - Paragraphs | |
| * - Bold/italic (**text**, *text*) | |
| * - Inline code (`code`) and code blocks (```) | |
| * - Bullet lists (- item) | |
| * - Blockquotes (> text) | |
| * - Horizontal rules (---) | |
| * Backed by `marked` (CommonMark + GFM) so lessons can use tables, ordered | |
| * and nested lists, links, and images, none of which the hand-rolled | |
| * predecessor of this module supported. | |
| * | |
| * No JSX components — pure markdown. For interactive components | |
| * (e.g., drag-drop simulations) we'll need a real MDX setup, but the | |
| * current AMPH content is markdown-only. | |
| * No JSX/component support — pure markdown. Interactive components would | |
| * need a real MDX compile step; current AMPH content is markdown-only. | |
| * | |
| * Security: all output is HTML-escaped. No raw HTML passes through. | |
| * Security: raw HTML (block or inline) is escaped rather than passed | |
| * through, and link/image URLs are restricted to http(s), mailto, and | |
| * relative/anchor paths — everything else is dropped to its text content. | |
| * MDX (markdown subset) -> HTML renderer for lesson content. | |
| * | |
| * Backed by `marked` (CommonMark + GFM) so lessons can use tables, ordered | |
| * and nested lists, links, and images, none of which the hand-rolled | |
| * predecessor of this module supported. | |
| * | |
| * No JSX/component support. This is pure markdown. Interactive components would | |
| * need a real MDX compile step; current AMPH content is markdown-only. | |
| * | |
| * Security: raw HTML (block or inline) is escaped rather than passed | |
| * through, and link/image URLs are restricted to http(s), mailto, and | |
| * relative/anchor paths. Everything else is dropped to its text content. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/mdx.ts` around lines 2 - 13, Replace the em dashes in the
module-level comments of the MDX renderer with permitted punctuation, preserving
the existing descriptions and meaning.
Source: Coding guidelines
Summary
Resolves the second P0 in
docs/CONTENT-AUDIT-2026-07-16.md: the lesson renderer couldn't display tables, ordered lists, nested lists, links, or images used throughoutcontent/curriculum/modules/(28 files use tables, 23 use ordered lists, one has 3-level nested lists in a decision flowchart). Multi-line blockquote callouts (the🎯 Analogy/📌 Key Takeawaypattern used throughout) also rendered as several broken adjacent<blockquote>tags instead of one block with its nested list intact.src/lib/mdx.tswithmarked(CommonMark + GFM, zero runtime deps) behind a custom renderer.http(s),mailto, and relative/anchor paths — everything else drops to its text content — preserving the "no raw HTML" security posture of the original renderer.table/th/td/a/img, none of which existed before since the old renderer never emitted them.src/lib/__tests__/mdx.test.tsto cover tables, ordered/nested lists, links (safe + unsafe schemes), images (safe + unsafe schemes), multi-line blockquote callouts, and raw-HTML escaping.Video embeds remain out of scope, per the audit's own note not to start the video program until a lesson can render and track a video correctly — no current lesson content uses them.
Test plan
pnpm tsc --noEmit— zero errorspnpm lint— zero errors (same 6 pre-existing warnings)pnpm test— 215/215 passing, including 26 rewritten MDX testspnpm build— production build succeeds🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
New Features
Security
Bug Fixes