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/</</g; $content =~ s/>/>/g; $content =~ s/"/"/g; $content =~ s/&/&/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 (<
, >
, "
, &
) 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.