I kept bouncing between Markdown tools: one for quick previews, another for math, another for Mermaid diagrams, and none of them let me paste the result into Gmail or Google Docs without breaking the formatting. Most also wanted an account before I typed a single line.
So I built Markdown Preview, a free live Markdown editor that runs entirely in the browser.
What it does
- Live side-by-side preview with synced scrolling and full GitHub Flavored Markdown (tables, task lists, footnotes, emoji, GitHub-style callouts, YAML frontmatter)
-
Math with KaTeX:
$x^2$inline,$$...$$for blocks - Mermaid diagrams: flowcharts, sequence diagrams, Gantt charts, following your light/dark theme
- Syntax highlighting for 180+ languages with a copy button on every code block
-
Slides: a
---line starts a new slide in present mode, with speaker notes - Copy as rich text into Gmail, Google Docs or Slack with headings, tables and bold intact (great for AI chat output too)
-
Open any GitHub, GitLab or Bitbucket file from a link, or share
/?url=your-file.md - Tabs, outline, command palette, find and replace, auto snapshots (version history), custom preview CSS
- Export to PDF, standalone HTML or .md
Privacy first
There is no account and no backend for your documents. Everything is stored in your browser (IndexedDB + localStorage), and it installs as a PWA, so it works offline too.
Try it
π https://mdpreview.jignesh.dev/
It's free, no tiers and no watermark. I'd love feedback: what's missing for your Markdown workflow, and what would make you switch from your current editor?
Top comments (3)
Copy as rich text into Gmail and Docs is the feature I'd actually switch for. That paste is where most Markdown tools fall apart.
One question on
/?url=your-file.md: since GFM allows raw HTML, are you sanitizing the rendered output (DOMPurify or similar) for files opened from a link? Everything lives in IndexedDB on your origin, so a shared link to a crafted .md file is the one path where someone else's content runs next to the user's saved documents. Rendering Mermaid in itsstrictsecurity level and passing the HTML through a sanitizer after markdown-it/remark usually covers it.A nice extra for that feature: show the source URL in a small bar above the preview, so people know which file they're looking at when a link gets passed around.
Thanks, glad the Gmail and Docs paste works for you. That's the part I care most about too.
On /?url=: yes, everything is sanitized with DOMPurify, whatever the source. Scripts, event handlers, javascript: links, styles and forms are stripped. Mermaid runs in strict mode, KaTeX with trust off, embeds are sandboxed, and there's a Content Security Policy on top. Your question made me test it properly, with a file of about 60 known attacks run in a real browser, and it also helped me close a couple of smaller gaps. Thank you for that.
Your source bar idea is live now: a document opened from a link shows where it came from, marks it once you edit it, and has a button to copy a link that opens the same file here.
That's a thorough answer, and nice to see the source bar shipped already.
Two things I'd do with that 60-attack file now that it exists:
Keep it as a regression test. A Playwright test that loads the file through /?url= and asserts no dialog fired and no script or on* attributes are in the preview DOM. The risky moments are dependency bumps: Mermaid and DOMPurify have both had sanitizer bypasses fixed in minor releases, and a quiet upgrade to markdown-it or a KaTeX config change can reopen something you closed.
Check the order of steps. Anything that changes the HTML after DOMPurify runs (Mermaid swapping code blocks for SVG, KaTeX rendering, syntax highlighting, adding heading anchors) happens outside the sanitizer. Mutation XSS usually lives in exactly that gap. Either sanitize last or run those outputs through DOMPurify too (the SVG profile for Mermaid).
One more on rich-text copy: the HTML that goes to the clipboard leaves your CSP behind, since Gmail and Docs get the raw markup. It's worth making sure the copy path reads from the sanitized DOM and not a pre-sanitize string.