Skip to content

fix(markdown): skip placeholder text for empty files (#434)#2719

Merged
nperez0111 merged 1 commit intomainfrom
feat/skip-empty-file-blocks
May 7, 2026
Merged

fix(markdown): skip placeholder text for empty files (#434)#2719
nperez0111 merged 1 commit intomainfrom
feat/skip-empty-file-blocks

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented May 6, 2026

Summary

Empty image/video/audio/file blocks (no url set) no longer leak their "Add image"/"Add video"/etc. UI text into HTML and markdown exports — fixes #434.

Rationale

When a user inserts a media block but hasn't selected a file yet, the block renders an "Add image" placeholder UI in the editor. Previously that placeholder text was being passed through to the external HTML / markdown exporters as a literal <p>Add image</p> paragraph, which is a confusing artifact in any exported document.

Changes

  • Each block's toExternalHTML (Image, Video, Audio, File) now emits the bare element (<img>, <video>, <audio>, <embed>) with no src attribute when props.url is empty, instead of a paragraph containing the UI placeholder text.
  • htmlToMarkdown no longer emits the stale "Add image" / "Add video" fallback strings; empty media now emits \n\n to preserve block-level spacing consistent with other empty-content serializers (paragraph, heading), and a new serializeEmbed case handles file placeholders.
  • Snapshot files updated to reflect the new export output.

Impact

The editor UI is unaffected — the "Add image" / "Add file" buttons are rendered by render (via createAddFileButton), not toExternalHTML, so users still see the upload UI as before. Round-trip works because the existing tag-name parsers (imageParse, videoParse, audioParse, fileParse) already handle empty src by returning url: undefined, so an empty block exported to HTML re-imports as the same empty block. Markdown round-trip cannot preserve empty placeholder blocks (markdown has no syntax for them), but the placeholder text no longer appears.

Testing

All 842 existing format-conversion tests pass; the image/button, video/button, audio/button, and file/button snapshots were updated and verify the new <img /> / <video></video> / <audio></audio> / <embed /> output.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added. (Existing button-state snapshots updated)
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for embed elements in content export
  • Bug Fixes

    • Improved handling of media and file blocks without URLs—now displays cleaner empty blocks instead of placeholder prompts for images, videos, audio, and files

Empty image/video/audio/file blocks (no url set) previously leaked
their "Add image"/"Add video"/etc. UI text into HTML and markdown
exports. Each block's toExternalHTML now emits the bare element
(<img>, <video>, <audio>, <embed>) with no src, so the placeholder
text is gone from exports while the block still round-trips back to
an empty block on re-import via the existing tag-name parsers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview May 6, 2026 8:59pm
blocknote-website Ready Ready Preview May 6, 2026 8:59pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

The PR changes media blocks (Image, Video, Audio, File) and the markdown exporter to handle missing source URLs by returning empty elements instead of placeholder text, and adds embed element support to the markdown serialization pipeline.

Changes

Media Block Fallbacks & Markdown Embed Support

Layer / File(s) Summary
Block Rendering
packages/core/src/blocks/Image/block.ts, packages/core/src/blocks/Video/block.ts, packages/core/src/blocks/Audio/block.ts, packages/core/src/blocks/File/block.ts
When source URL is missing, return empty HTML elements (<img>, <video>, <audio>, <embed>) instead of placeholder paragraphs like "Add image" or "Add video".
Markdown Export
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
Updated image, video, and audio serialization to emit blank lines when src is absent; added new serializeEmbed function to handle embed elements; introduced embed tag routing in the main element serializer switch.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 No more placeholders cluttering the way,
Empty elements now save the day,
Media blocks hide when sources lack,
Embeds join the pack, setting the track!
Markdown flows clean, without distraction,
A hop toward prettier content satisfaction!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(export): skip placeholder text for empty file blocks (#434)' clearly and specifically describes the main change: preventing placeholder text from appearing in exports for empty file blocks.
Description check ✅ Passed The description is comprehensive and follows the template structure with all major sections completed (Summary, Rationale, Changes, Impact, Testing, Checklist), providing clear context about the problem, solution, and implementation.
Linked Issues check ✅ Passed The changes fully address the linked issue #434 by preventing placeholder UI text ('Add image', 'Add video', 'Add audio', 'Add file') from leaking into HTML and Markdown exports.
Out of Scope Changes check ✅ Passed All changes are focused and in-scope: modifications to Image, Video, Audio, File blocks' toExternalHTML methods and htmlToMarkdown serialization directly support the objective of removing placeholder text from exports.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/skip-empty-file-blocks

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 6, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2719

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2719

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2719

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2719

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2719

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2719

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2719

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2719

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2719

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2719

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2719

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2719

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2719

commit: 03e0a74

Copy link
Copy Markdown

@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 (3)
packages/core/src/blocks/Image/block.ts (1)

143-147: ⚡ Quick win

Add alt="" to the empty placeholder <img> for a11y/HTML conformance.

A bare <img> with neither src nor alt is flagged by HTML validators and is treated by assistive technologies as an image of unknown content. Setting an empty alt marks it as decorative and is the standard way to express "no meaningful image":

♻️ Proposed fix
     if (!block.props.url) {
+      const placeholder = document.createElement("img");
+      placeholder.alt = "";
       return {
-        dom: document.createElement("img"),
+        dom: placeholder,
       };
     }
🤖 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 `@packages/core/src/blocks/Image/block.ts` around lines 143 - 147, The
placeholder image returned when block.props.url is falsy currently creates an
<img> without alt text; update the fallback in the Image block (the branch
checking block.props.url in block.ts) to return an <img> element with alt=""
(i.e., set the element's alt attribute to an empty string) so the placeholder is
marked decorative and conforms to a11y/HTML validation.
packages/core/src/blocks/File/block.ts (1)

76-81: 💤 Low value

Bare <embed> works for round-trip but may render a small visual artifact.

<embed> without src is well-suited for the round-trip path (fileParse already handles EMBED), so functionally this is fine. Note that some browsers render a small empty plugin region for <embed> lacking a meaningful src, unlike <img>/<video>/<audio> which collapse cleanly. If any downstream HTML consumers are strict about validation or visible artifacts, you could optionally add a hidden marker (e.g., style="display:none" or a data-blocknote-empty attribute) to make intent explicit. Not a blocker — current behavior already meets the issue #434 objective of removing the placeholder text.

🤖 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 `@packages/core/src/blocks/File/block.ts` around lines 76 - 81, The
toExternalHTML function currently returns an <embed> element with no src when
block.props.url is missing, which can render a small empty plugin region in some
browsers; update toExternalHTML (in the File block) so that when
!block.props.url you still create the embed DOM but add a hidden marker (e.g.,
set style.display="none" or set a data attribute like
data-blocknote-empty="true") to avoid visible artifacts and make the intent
explicit for downstream consumers.
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts (1)

520-531: 💤 Low value

Optional: deduplicate serializeAudio and serializeEmbed.

The two functions are now byte-identical (empty-src → "\n\n", otherwise ctx.indent + \\n\n``). You could collapse them into a shared helper to make future changes (e.g., adding alt-text-style handling) consistent across both block types.

♻️ Possible refactor
-function serializeAudio(el: HTMLElement, ctx: SerializeContext): string {
-  const src = el.getAttribute("src") || "";
-  if (!src) {return "\n\n";}
-  // Audio has no visible representation in markdown; output as link with empty text
-  return ctx.indent + `[](${src})\n\n`;
-}
-
-function serializeEmbed(el: HTMLElement, ctx: SerializeContext): string {
-  const src = el.getAttribute("src") || "";
-  if (!src) {return "\n\n";}
-  return ctx.indent + `[](${src})\n\n`;
-}
+function serializeSrcAsBlockLink(
+  el: HTMLElement,
+  ctx: SerializeContext,
+): string {
+  const src = el.getAttribute("src") || "";
+  if (!src) {return "\n\n";}
+  // No visible markdown representation; emit empty-text link.
+  return ctx.indent + `[](${src})\n\n`;
+}
🤖 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 `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 520
- 531, serializeAudio and serializeEmbed are identical; extract the shared
behavior into a helper (e.g., serializeSrcAsLink or formatMediaAsLink) that
takes an HTMLElement and SerializeContext, returns "\n\n" when src is empty else
ctx.indent + `[](${src})\n\n`, then update serializeAudio and serializeEmbed to
call that helper so future changes (like alt-text handling) are applied in one
place; reference functions serializeAudio, serializeEmbed, and the new helper
name in your change.
🤖 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.

Nitpick comments:
In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts`:
- Around line 520-531: serializeAudio and serializeEmbed are identical; extract
the shared behavior into a helper (e.g., serializeSrcAsLink or
formatMediaAsLink) that takes an HTMLElement and SerializeContext, returns
"\n\n" when src is empty else ctx.indent + `[](${src})\n\n`, then update
serializeAudio and serializeEmbed to call that helper so future changes (like
alt-text handling) are applied in one place; reference functions serializeAudio,
serializeEmbed, and the new helper name in your change.

In `@packages/core/src/blocks/File/block.ts`:
- Around line 76-81: The toExternalHTML function currently returns an <embed>
element with no src when block.props.url is missing, which can render a small
empty plugin region in some browsers; update toExternalHTML (in the File block)
so that when !block.props.url you still create the embed DOM but add a hidden
marker (e.g., set style.display="none" or set a data attribute like
data-blocknote-empty="true") to avoid visible artifacts and make the intent
explicit for downstream consumers.

In `@packages/core/src/blocks/Image/block.ts`:
- Around line 143-147: The placeholder image returned when block.props.url is
falsy currently creates an <img> without alt text; update the fallback in the
Image block (the branch checking block.props.url in block.ts) to return an <img>
element with alt="" (i.e., set the element's alt attribute to an empty string)
so the placeholder is marked decorative and conforms to a11y/HTML validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fe6d04d7-e1e5-4ee7-9a44-6e17b1d4d348

📥 Commits

Reviewing files that changed from the base of the PR and between b0d0985 and 03e0a74.

⛔ Files ignored due to path filters (9)
  • tests/src/unit/core/clipboard/copy/__snapshots__/text/html/basicBlocks.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/file/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/image/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/button.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/button.md is excluded by !**/__snapshots__/**
  • tests/src/unit/react/formatConversion/export/__snapshots__/html/reactFile/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/button.html is excluded by !**/__snapshots__/**
📒 Files selected for processing (5)
  • packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
  • packages/core/src/blocks/Audio/block.ts
  • packages/core/src/blocks/File/block.ts
  • packages/core/src/blocks/Image/block.ts
  • packages/core/src/blocks/Video/block.ts

@nperez0111 nperez0111 changed the title fix(export): skip placeholder text for empty file blocks (#434) fix(markdown): skip placeholder text for empty files (#434) May 7, 2026
@nperez0111 nperez0111 merged commit 531ea32 into main May 7, 2026
23 checks passed
@nperez0111 nperez0111 deleted the feat/skip-empty-file-blocks branch May 7, 2026 04:01
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.

Markdown / HTML exporter and hidden blocks

1 participant