Skip to content

fix(lesson): stop a code sample from blanking the whole lesson, and never leave a dead video frame (#566) - #573

Merged
guillermoscript merged 1 commit into
masterfrom
fix/lesson-renderer-codeblock-video-566
Jul 27, 2026
Merged

fix(lesson): stop a code sample from blanking the whole lesson, and never leave a dead video frame (#566)#573
guillermoscript merged 1 commit into
masterfrom
fix/lesson-renderer-codeblock-video-566

Conversation

@guillermoscript

Copy link
Copy Markdown
Owner

Closes #566.

1. A module in <CodeBlock> dumped the entire lesson as raw source

MDX parses a JSX element's children as MDX, so a snippet whose first line starts at column 0 with export/import was read as an ESM statement and handed to acorn, which choked on </CodeBlock> and failed the whole document. The student got unrendered <Quiz options={[...]}> tags and pipe tables behind a banner.

inlineCodeBlockBodies() moves the snippet into a code prop as code={JSON.parse('…')} — the escape hatch the block editor's serializer already uses for complex props — so nothing inside a snippet reaches acorn. {expressions} and <tags> inside code broke identically and are fixed by the same move.

It also renders more faithfully than before: as children, **bold** inside a snippet was parsed as markdown and the asterisks were lost on the way to the <pre>.

The web app fails the same way — verified, not assumed

The issue flagged this as unchecked. next-mdx-remote-client's serialize() uses the same parser and fails on the same input:

export-first: FAIL — [next-mdx-remote-client] error compiling MDX
import-first: FAIL — [next-mdx-remote-client] error compiling MDX

So the normalization runs on both paths — serializeLessonMdx() (student page) and MDXPreview (teacher preview) — and both CodeBlock components accept the prop. tests/unit/lesson-mdx-serialize.test.ts covers the app half.

The two implementations live in separate npm projects that cannot import each other, so a test asserts mcp-server/.../mdx.ts and lib/lesson/mdx-source.ts produce byte-identical output.

2. One unparseable block no longer costs the rest of the lesson

parseLessonBlocks() parses the document whole first and keeps it as a single block on success — ordinary lessons and <Steps> numbering are untouched. Only when the document fails does it split, on blank lines outside fenced code (an unterminated fence parses happily to EOF, so a bad split would silently swallow the rest), re-joining greedily so multi-line elements like <Steps> survive their internal blank lines.

3. A blocked video left a 400px black void

Widget hosts refuse to frame YouTube/Vimeo, and the link fallback only fired for URLs we could not embed at all — so the common case got no message, no link, nothing. There is no reliable load event inside the sandbox to tell a working frame from a blocked one, so the link is now always offered under the player, in both LessonBody and the authored <Video> component.

<Embed> has the same shape of defect for arbitrary URLs and is not touched here.

Fixture

broken-mdx keeps both states inspectable, per the issue: the module now renders with its contador.tsx label, and an unclosed <Callout> next to it exercises the per-block fallback while the prose and table around it render.

Verified

  • 12 new tests fail against the pre-fix tree (checked out HEAD, ran them, 12 red / 15 pre-existing green), then pass after
  • npm run test:unit616 passed, 50 files
  • mcp-server39 passed
  • npm run build (Next) and mcp-server build — both green, 18 widgets
  • tsc --noEmit clean in both projects
  • Both fixture variants re-rendered headlessly through the inspector

Note on MDXPreview

Its compile effect is restructured only because lint-staged lints the whole file and it carried a pre-existing set-state-in-effect error. The compiled output is now tagged with the source it came from, so "loading" and "empty" are derived instead of set synchronously in the effect body. No behaviour change.

🤖 Generated with Claude Code

https://claude.ai/code/session_017sLtN12VfnLziCpst3Pfo2

…ever leave a dead video frame (#566)

Two defects in the shared lesson renderer, both surfaced by the widget
preview fixtures.

**A module in <CodeBlock> dumped the entire lesson as raw source.**
MDX parses a JSX element's children as MDX, so a snippet whose first
line starts at column 0 with `export`/`import` was read as an ESM
statement and handed to acorn, which then choked on `</CodeBlock>` and
failed the *whole document*. That is the documented usage — pasting a
module into `<CodeBlock language="tsx">` is the obvious thing a teacher
does — and it cost the student every other block on the page.

`inlineCodeBlockBodies()` moves the snippet into a `code` prop using
`JSON.parse('…')`, the escape hatch the block editor's serializer
already uses for complex props, so nothing inside a snippet reaches
acorn — `{expressions}` and `<tags>` broke identically. It also renders
more faithfully: as children, `**bold**` inside a snippet was parsed as
markdown and the asterisks were lost on the way to the <pre>.

The web app compiles the same source with next-mdx-remote-client and
failed on the same input (verified, not assumed), so the normalization
runs on both paths — the student page and the teacher preview — and
both CodeBlock components accept the prop. `title="…"` now labels a JSX
block the way it already labelled a fenced one.

**One unparseable block no longer costs the rest of the lesson.**
`parseLessonBlocks()` still parses the document whole and keeps it as a
single block on success, so ordinary lessons and <Steps> numbering are
untouched. Only when the document fails does it split on blank lines
outside fenced code, re-joining greedily so multi-line elements survive,
and degrade just the block that will not parse.

**A blocked video left a 400px black void.** Widget hosts refuse to
frame YouTube/Vimeo, and the link fallback only fired for URLs we could
not embed at all — so the common case got no message, no link, nothing.
There is no reliable load event inside the sandbox to tell a working
frame from a blocked one, so the link is now always offered under the
player, in both LessonBody and the authored <Video> component.

The `broken-mdx` fixture keeps both states inspectable: the module now
renders, and an unclosed <Callout> next to it shows the per-block
fallback.

MDXPreview's compile effect is restructured only because lint-staged
lints the whole file and it carried a pre-existing set-state-in-effect
error; the compiled output is now tagged with its source so "loading"
and "empty" are derived instead of set synchronously in the effect.

Verified: 12 new tests fail against the pre-fix tree; 616 root + 39
mcp-server tests pass; both builds green; both fixture variants
re-rendered in the inspector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sLtN12VfnLziCpst3Pfo2
@guillermoscript
guillermoscript merged commit a6c88d3 into master Jul 27, 2026
2 checks passed
@guillermoscript
guillermoscript deleted the fix/lesson-renderer-codeblock-video-566 branch July 27, 2026 20:09
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.

lesson renderer: export inside <CodeBlock> dumps the whole lesson as raw source, and a blocked video leaves a 400px void

1 participant