fix(lesson): stop a code sample from blanking the whole lesson, and never leave a dead video frame (#566) - #573
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #566.
1. A module in
<CodeBlock>dumped the entire lesson as raw sourceMDX parses a JSX element's children as MDX, so a snippet whose first line starts at column 0 with
export/importwas 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 acodeprop ascode={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'sserialize()uses the same parser and fails on the same input:So the normalization runs on both paths —
serializeLessonMdx()(student page) andMDXPreview(teacher preview) — and bothCodeBlockcomponents accept the prop.tests/unit/lesson-mdx-serialize.test.tscovers the app half.The two implementations live in separate npm projects that cannot import each other, so a test asserts
mcp-server/.../mdx.tsandlib/lesson/mdx-source.tsproduce 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
LessonBodyand the authored<Video>component.<Embed>has the same shape of defect for arbitrary URLs and is not touched here.Fixture
broken-mdxkeeps both states inspectable, per the issue: the module now renders with itscontador.tsxlabel, and an unclosed<Callout>next to it exercises the per-block fallback while the prose and table around it render.Verified
HEAD, ran them, 12 red / 15 pre-existing green), then pass afternpm run test:unit— 616 passed, 50 filesmcp-server— 39 passednpm run build(Next) andmcp-serverbuild — both green, 18 widgetstsc --noEmitclean in both projectsNote on
MDXPreviewIts compile effect is restructured only because lint-staged lints the whole file and it carried a pre-existing
set-state-in-effecterror. 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