fix(web): hide YAML frontmatter in Markdown file previews#23914

open4
main
fix/issue-23704
opened2d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 1d ago

Problem

Opening a Markdown file with leading YAML frontmatter from Files renders the metadata as a horizontal rule and a large setext heading. The document should start at its body heading.

Change

Mask complete leading YAML frontmatter in web/desktop file and attachment previews. Preserve every source offset and line break so task checkbox edits still target the original file. Source view and saved contents retain the metadata. Recognize BOM-prefixed files and LF, CRLF, or CR line endings.

Scope and approval

Fixes #23704

Triaged bug via the automated issue flow. Reproduced on current main in the real web app with the reporter's exact fixture. Mobile uses a separate renderer and is outside this reported web/desktop fix.

Verification

  • vp test run src/components/files/FilePreviewPanel.test.ts from apps/web: 17 passed. BOM and CR-only cases failed before the follow-up fix.
  • Targeted lint and formatting passed; web tsc --noEmit -p apps/web passed.
  • Real app: Files preview and draft attachment preview at 1440 and 390 CSS pixels, light and dark. Source/render toggles and task checkbox persistence checked with paired recordings and byte comparisons.

Evidence

Evidence index. Independent proof audit reproduced F1-after pixel-identically and verified all 36 artifact hashes and all eight diff pairs.

Same reporter bytes, route /draft/52c1b0b1-6ff3-4fb8-990a-967b991c033f, layout and theme within each still pair. Files panel maximized, file explorer collapsed. Chrome on Linux; themes follow browser prefers-color-scheme. No device emulation; default font scaling. CSS viewports 1440×900 and 390×900, zoom 100%, DPR 2. Browser tool reduces desktop screenshots to 1280×800; narrow screenshots are 780×1800. These are responsive web captures, not native mobile.

Diff0 = per-channel absolute difference amplified 8×, no threshold. DiffT = magenta where maximum RGB channel difference >16/255, black elsewhere. No alignment, crops, masks, or exclusions. Pixel counts include app chrome. Artifact SHA-256 is shown beneath every link. Before code: 51273020db; after code: 373457aebc (subsequent commits only add fixtures).

Row / CSS viewportBefore (51273020db)After (373457aebc)Diff0DiffTChanged pixels (0 / T)
F1 1440×900 lightF1-before.png
SHA-256 ff07910f24e0cc1f7e6ccc6926247bcfa597ffe7c7a296ac9eaaf5a0c1b42f71
F1-after.png
SHA-256 c992bd4f49ab6f723b8e72f8263f7425665fe2b6634f0133dbb5318cd9ece8dd
F1-diff0.png
SHA-256 cda2bf2aa37ae82be22a3b7b10ecbdfa9a804dc0c0624d303cf1fc9239ea3447
F1-diffT.png
SHA-256 c1abfcc2f4fa3bd3b3db8bb399285e034061c182b972e009ceddd7ab83ee96e7
18688 / 15900
F2 1440×900 darkF2-before.png
SHA-256 860eb814b6acd7d99f9b7f13a5975d3c0645b65129e1013ea1907f0d6cb79f6d
F2-after.png
SHA-256 df35ec8d321ee7933d8968e606a101ec172cfdae5e97368f95107f736109b0ef
F2-diff0.png
SHA-256 a79dbe8dcf61d91a86bd0cdb028b1bca899f5bf28a4e7b0b240d2abc3c14d6e4
F2-diffT.png
SHA-256 c31d4d70989fb9ef0414886ab8018a006533998ee6dd1699420da103939f8c62
18688 / 16333
F3 390×900 lightF3-before.png
SHA-256 b78f71fe2b4b99e7f8ca4a09fde6c757ae229573b93559368e729b5bb7292d59
F3-after.png
SHA-256 18dfab21f272c256577319391df03c49d56c70f95464d1235b7662d35422b093
F3-diff0.png
SHA-256 31540e8cbb89b0c436f765defb3195ad0e860716cc24ba28d0785c0011159ff1
F3-diffT.png
SHA-256 cb0a7aa17e64479371583a05ac4f81110f85849701b790fe010b4d788c9ba00d
70422 / 67809
F4 390×900 darkF4-before.png
SHA-256 3f87c396438b21af650040b7e756068022aa0400277e5fe1fb321c12c09481ef
F4-after.png
SHA-256 c6ab8d254d0a8c22d95c3b020d9a5e98a5772cf49525225537e1ab6d89941a20
F4-diff0.png
SHA-256 0f28f66ebe6abd71b8ded5f108943461004e17be85c1fb110e01120e3ee28f1c
F4-diffT.png
SHA-256 fd5067a064caba83dad7431a710a8e59fc03aee8b0ab9f9684cd703738334ad1
70428 / 67558
D1 1440×900 lightD1-before.png
SHA-256 4aea81677b88525892e48a05a2e541e3b645d8dea052919450abaff621becd95
D1-after.png
SHA-256 cccd2d652e1510f807b34798bd7c63d11aa4cc8ae30c53a9171854ffaa9b0c22
D1-diff0.png
SHA-256 b7d4aca41403010d1a466ef431edbe79a3e90947710a489dbfaff81a08f5b438
D1-diffT.png
SHA-256 9caa16e11466232de9dc879070b8cf38f8b3c09faab317bb5bc64e9737a76817
17596 / 16118
D2 1440×900 darkD2-before.png
SHA-256 429e56e2e7ac2c7d8433fc60c5dc333ec515914b09d33f480763c663de227d81
D2-after.png
SHA-256 ca067553db6e20b960e3fa2624305e5f0fe78dc8fd58ebca0b9c45a0b2a15203
D2-diff0.png
SHA-256 a4ebb2def446ec5ddfd2eaf40f8cb4e7bb4b90df3467c194d46ae7ae0511ab2c
D2-diffT.png
SHA-256 c8694bb60b3a67bc22bcc8417ec7b6831f2d9facfc341b0b25fc9e2b2239cead
17545 / 15605
D3 390×900 lightD3-before.png
SHA-256 4db0a91e251874cf65da7fe377db1d47297d8c4bff8b2d41afc710814c712ae0
D3-after.png
SHA-256 5b10e7d64a8e311a548a400b6ff2190d480c871219346fcf403283c6f9c4819a
D3-diff0.png
SHA-256 90248f64d4a115dedd583b27deee77868810862d5a2cf6883ffa728e0cb8dec1
D3-diffT.png
SHA-256 a0d8d8e557d3f93218addc288f30ae3866dd1880102d0dc23b2d5d40ab9e7f59
71442 / 68805
D4 390×900 darkD4-before.png
SHA-256 dd1bdf9eb89d99e2f865659e5f853a8635564ef03a29ceb6e9b41d00af2b50dc
D4-after.png
SHA-256 0e1556650674b6e938ef6987ce5f02c31dffd3b6986d4a7fb804fd4e18fb4ad1
D4-diff0.png
SHA-256 a808cd213aea78e331d3d146f27f88d9b18e0feab25b9fb0d33f782d2c237b84
D4-diffT.png
SHA-256 eac32bfe6958d4b5cb43f45660b3be32214fdff233a731cc6ef3f047d9746064
71443 / 68446

F1–F4: Files rendered preview passes: one body heading, no metadata/rule/setext artifact or masked gap. At 390 the body wraps. D1–D4: supplementary draft Blob attachment pairs show the same corrected body using AttachmentFilePreview; these do not establish sent asset fetching.

I1 task interaction, 1440×900 light. Same companion bytes and ordered actions. Timing and sidebar/composer framing differ between recordings; source syntax highlighting loads asynchronously. Recordings are normal speed. Approximate timestamps:

  • Before (14.109 s): 0–7 rendered → source with YAML intact; 7–9 rendered → first task checked → source; 9–11 source → rendered → uncheck; 11–14 close/reopen, explorer expanded.
  • After (19.158 s): 0–6 rendered → source with YAML intact; 6–9 rendered → first task checked → source; 9–16 source → rendered → uncheck; 16–19 close/reopen, explorer expanded.
  • Both versions: disk-byte assertion after checking equals original reporter bytes plus - [x] first task and - [ ] second task; after unchecking, byte comparison equals seeded original. No YAML or second-task mutation.
InteractionBeforeAfter
I1 Files task/sourceI1-before.mp4
SHA-256 e30e9b7667748eaa5009da0e1995ef51cdbc5c50b3bd8772e934a41ea469246c
I1-after.mp4
SHA-256 e7b5a718458c186f6574c48708cae47e2add49922432b4bcac634f2d9bf15a03
Draft source/render/close/reopen (supplementary)draft-before.mp4
SHA-256 37b95ab545fec971fabc92f91cdc6ed2123e35b96fcc2b60086015d9c8087798
draft-after.mp4
SHA-256 192824ad506ae632f79e6359bd2f94ae2b03d467122a685f51df2cd1ebb76f70

Draft interaction, 390×900 dark: before 9.230 s; after 7.727 s. At 0 s switch rendered → source; approximately 1–6 s inspect original YAML, closing fence, body heading; approximately 6–8 s return rendered → close → reopen. Before shows metadata in rendered view; after hides it. No task editing in attachment preview. Source syntax highlighting briefly loads, then displays original metadata.

L1 passes: real composer file intake, draft chip, rendered/source/close, exact reporter fixture. C1 passes in Files and draft: Control heading, Paragraph, body horizontal rule, Body after rule, checkbox retained. C2 visual proof is not claimed: formatting normalized its opener to ***; the focused unit test covers true unterminated frontmatter.

Unverified: A1–A4 and I2 sent attachment. No persisted sent fixture was available in this tab's isolated draft state. Sending via composer would invoke a provider; no provider was invoked and no synthetic sent state was injected. Draft proof is supplementary, not a substitute for these matrix rows. Desktop shares web components but was not separately launched. Native mobile, relay/tunnel, other providers, chat Markdown and other preview formats were not exercised; mobile renderer remains unchanged.

Visible differences: the frontmatter rule and metadata heading disappear, and the document body moves up to the beginning of the preview. Source metadata and task edits remain intact.

Reproduce with node apps/web/test/fixtures/frontmatter-preview/seed.mjs <workspace-root>, then open frontmatter-repro.md from Files or attach it in the composer. The committed seed recreates the exact reporter fixture and task companion.

Models: Claude Opus 5.5 (Claude Code harness) and GPT-6.1-Sol (Codex harness).

Checks

All checks passed

Added 4 commits

  • fix(web): frontmatter after a BOM or with CR line endings is hidden too373457a
  • test(web): include exact reporter fixture in preview seed9102852
  • fix(web): rendered markdown previews hide YAML frontmatter91f5ca1
  • test(web): add reproducible frontmatter preview fixtureseac2c78
Synced from GitHub

Approvability

Verdict: Approved at 9102852

Macroscope's review found this PR approvable — Adds a small, pure frontmatter-masking helper to the two existing Markdown preview paths, preserving source offsets and line endings for task interactions. The accompanying unit tests and fixtures cover the supported cases without introducing broader runtime, schema, or infrastructure changes.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 51273020db3245f4ec29514a419d29b5dcb012dc and 9102852665604e7354f00c871e063e648f7dc007.

📒 Files selected for processing: 9
  • apps/web/src/components/files/AttachmentFilePreview.tsx
  • apps/web/src/components/files/FileMarkdownPreview.tsx
  • apps/web/src/components/files/FilePreviewPanel.test.ts
  • apps/web/src/components/files/filePreviewMode.ts
  • apps/web/test/fixtures/frontmatter-preview/frontmatter-repro.md
  • apps/web/test/fixtures/frontmatter-preview/frontmatter-task-proof.md
  • apps/web/test/fixtures/frontmatter-preview/plain-control.md
  • apps/web/test/fixtures/frontmatter-preview/seed.mjs
  • apps/web/test/fixtures/frontmatter-preview/unterminated-control.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

📝 Walkthrough

Markdown previews now mask a leading YAML frontmatter block before rendering. The masking function preserves line breaks and character offsets. Tests cover supported fences, BOM and CR-only input, and unchanged input when fences are absent or not leading.

Changes: Markdown preview frontmatter

Layer / File(s)Summary
Detect and mask frontmatter
apps/web/src/components/files/filePreviewMode.ts, apps/web/src/components/files/FilePreviewPanel.test.ts, apps/web/test/fixtures/frontmatter-preview/*
maskMarkdownFrontmatter recognizes a leading --- block closed by --- or .... It replaces non-newline characters with spaces and leaves the input unchanged when the opening or closing fence is missing. Tests check preserved offsets and line counts, BOM and CR-only input, and unchanged cases.
Mask content in Markdown previews
apps/web/src/components/files/AttachmentFilePreview.tsx, apps/web/src/components/files/FileMarkdownPreview.tsx
Both preview components pass masked text to ChatMarkdown.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

Merge Risk: ⚪ Minimal · up to 91028

Leading frontmatter is hidden in web previews without changing saved metadata or task checkbox positions. The change is ready to merge after normal checks.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title "fix(web): hide YAML frontmatter in Markdown file previews" matches the main change. The diff masks leading YAML frontmatter in Markdown file and attachment previews, and the linked issue #2…
Description checkPassedThe description follows the repository template. It has Problem, Change, Scope and approval, and Verification sections. The problem is stated with expected behavior and a reproduction path. The change…
Linked Issues checkPassedIssue #23704 requires leading YAML frontmatter to be recognized so the rendered Markdown starts at the body heading. FileMarkdownPreview passes text through maskMarkdownFrontmatter before `ChatMar…
Out of Scope Changes checkPassedThe changed source files implement the #23704 preview behavior. The attachment preview uses the same masking helper, which is consistent with the stated web/desktop preview scope. The tests verify the…
Full details: Title check

Explanation

The title "fix(web): hide YAML frontmatter in Markdown file previews" matches the main change. The diff masks leading YAML frontmatter in Markdown file and attachment previews, and the linked issue #23704 asks for the same behavior. The title is short, specific, and follows the conventional commit style used in the repository.

Full details: Description check

Explanation

The description follows the repository template. It has Problem, Change, Scope and approval, and Verification sections. The problem is stated with expected behavior and a reproduction path. The change explains the masking approach, BOM and line-ending handling, and what stays intact. Scope cites the issue #23704 and explains why mobile is excluded. Verification lists the focused test run, lint, type check, and real-app checks with observed results, and it names what was not verified. The description does not include a conventional commit title line from the template, and the Scope section cites an automated triage flow rather than a maintainer approval comment, which the template asks for. These gaps are minor.

Full details: Linked Issues check

Explanation

Issue #23704 requires leading YAML frontmatter to be recognized so the rendered Markdown starts at the body heading. FileMarkdownPreview passes text through maskMarkdownFrontmatter before ChatMarkdown renders it. The helper masks complete leading --- frontmatter, preserves line breaks and character offsets, supports BOM and LF, CRLF, and CR line endings, and leaves unterminated or non-leading fences unchanged. Tests cover the rendered-content behavior, later offsets, BOM, CR-only input, and unchanged invalid cases. The implementation keeps source text available to the source view and preserves task marker offsets.

Full details: Out of Scope Changes check

Explanation

The changed source files implement the #23704 preview behavior. The attachment preview uses the same masking helper, which is consistent with the stated web/desktop preview scope. The tests verify the helper, and the Markdown fixtures and seed script provide focused reproduction and regression coverage. No unrelated behavior or unrelated product area is demonstrated. Mobile remains unchanged, consistent with the separate-renderer boundary.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Synced from GitHub

Thanks for picking this up. One UX suggestion: rather than hiding the frontmatter from the preview, could we render it the way GitHub's Markdown preview does, as a small key/value table above the body?

GitHub parses leading YAML frontmatter and shows it as a compact two-column table (type | Note, title | …, description | …, status | draft), then renders the document from its first real heading. That fixes the original bug (no stray <hr> plus giant setext heading) and still lets you see the metadata while reading.

For notes vaults (Obsidian/OKF-style files with type, status, description, tags), the frontmatter is often the most useful part when you skim files in the preview. With it hidden, you have to switch to Source view just to check a file's status.

Possible shape:

  • Keep the offset-preserving mask you already have, so task checkbox edits still target the right bytes.
  • Parse the masked range as YAML. If it parses into a flat map, render a small read-only table (or a collapsible "Properties" block) above the body. Show nested values as inline YAML/JSON.
  • If parsing fails, fall back to the current behaviour (hidden) or show it as a fenced yaml code block.

Happy to help test with real vault files if useful.

At 9102852, rendered previews hide complete frontmatter while Source retains it. A metadata table adds YAML parsing and a new preview UI; that suggestion is left for maintainer consideration as a follow-up. This fix stays scoped to removing the rule/setext rendering artifact.