markdown viewer

June 3, 2025

Since the latest Sonnet and Opus 4 release, I’ve been using Claude Code a lot more. It’s more restrained than the 3.7 release, and while its intermediate outputs can be wonky in long sessions, it usually lands on a solid result with help from linters, typecheckers, and tests.

One of the main uses I find it excellent at is producing reports and analysis on complex and unfamiliar codebases. I tell it to spin up subagents for analyzing different parts of the codebase, use git history extensively to read commit messages and PR descriptions, and then produce a report with mermaid graphs, code snippets, links to GitHub with line numbers etc.

I don’t mind working in the terminal but reading is something that browsers do better and turns out Markdown converts to HTML quite well, what do you know.

So I wrote generated a tiny script that is aliased to mdview, so I can quickly run

; mdview ./mcp-complete-guide.md
Press enter to clean up and exit...

And a browser window automatically open with the entire report generated in a nice GitHub-style markdown preview.

The other nice thing about it is that it cleans up after itself - no loose HTML files that pile up. Burn after reading.

The script:

#!/usr/bin/env bash
set -e

if [ $# -eq 0 ]; then
  echo "Usage: $0 [--debug] <markdown-file>"
  exit 1
fi

DEBUG=false
if [ "$1" = "--debug" ]; then
  DEBUG=true
  shift
fi

f=$(mktemp /tmp/mdpreview.XXXXXX)
mv "$f" "$f.html"
f="$f.html"
trap 'rm -f "$f"' EXIT

cat > "$f" <<EOF
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown-light.min.css">
  <style>
    body { box-sizing: border-box; min-width:200px; max-width:980px; margin:0 auto; padding:45px; }
    .markdown-body { box-sizing: border-box; min-width:200px; max-width:980px; margin:0 auto; padding:45px; }
  </style>
	<script type="module">
	import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
	mermaid.initialize({ startOnLoad: true, theme: 'default' });
	window.addEventListener('load', () => {
		console.log('Mermaid diagrams found:', document.querySelectorAll('.mermaid').length);
	});
	</script>
</head>
<body class="markdown-body">
EOF

pandoc --from=gfm --to=html "$1" | perl -0777 -pe 's|<pre class="mermaid"><code>(.*?)</code></pre>|my $content = $1; $content =~ s/&lt;/</g; $content =~ s/&gt;/>/g; $content =~ s/&quot;/"/g; $content =~ s/&amp;/&/g; "<div class=\"mermaid\">$content</div>"|gse' >> "$f"

echo "</body></html>" >> "$f"

if [ "$DEBUG" = true ]; then
  cat "$f"
else
  if command -v xdg-open >/dev/null; then
    xdg-open "$f"
  else
    open "$f"
  fi
  read -p "Press enter to clean up and exit..." _
fi

It’s simple enough but here are some worthy shoutouts:

Mermaid Rendering

I render Mermaid diagrams client-side by importing the ES module from CDN (https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs). Pandoc outputs mermaid blocks as <pre class="mermaid"><code> elements, which Mermaid.js doesn’t recognize, so I need to transform them.

The perl (yes) one-liner matches all <pre class="mermaid"><code>...</code></pre> blocks and converts them to <div class="mermaid"> elements. During conversion, it unescapes HTML entities (&lt;, &gt;, &quot;, &amp;) to restore the original Mermaid syntax. The -0777 flag tells perl to slurp the entire file, and the s modifier allows the regex to match across newlines.

Temporary File Cleanup

The script creates a temporary HTML file with mktemp /tmp/mdpreview.XXXXXX and immediately renames it to add the .html extension—browsers need this to render the page correctly. I use trap 'rm -f "$f"' EXIT to register a cleanup handler that removes the file when the script exits (even if you interrupt it). In interactive mode, the script waits for user input before cleanup so you can actually use the browser tab.

< Back