A lightweight local web app to browse and render Markdown files (.md, .markdown) from a folder. It can also view PDFs, HTML files, and images (.png, .jpg, .jpeg, .gif, .webp, .svg, .bmp, .ico, .avif) found in the same folder tree. Log files (.log, .jsonl, .ndjson) can be live-tailed, cleared, filtered, and — for JSON logs — rendered as tables.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/roraja/markdown-go/master/install.sh | shWindows (PowerShell):
iex (iwr 'https://raw.githubusercontent.com/roraja/markdown-go/master/install.ps1').ContentThen run:
mdviewer -root ~/your-notes -port 8080- Go 1.22+
From the project root:
go run . -root . -port 8080Then open:
http://localhost:8080
Use -root with the folder you want to scan:
go run . -root /path/to/your/folder -port 8080Example:
go run . -root ~/notes -port 8080Build a local binary:
mkdir -p bin
go build -o bin/mdviewer .Run it:
./bin/mdviewer -root ~/notes -port 8080source .vscode/commands.sh
InstallThis builds and installs mdviewer into the first writable directory in your PATH (or ~/.local/bin if needed).
go install .Then run the installed binary from your Go bin directory (for example $(go env GOPATH)/bin/mdviewer-go).
- Start the server with
go run .or the built/installed binary. - Open
http://localhost:8080in your browser. - Select a Markdown file from the left sidebar.
- Use Hide Sidebar for full-width reading, or toggle to Show Raw.
The sidebar live-updates every 5 seconds — new files, deletions, and modification time changes appear automatically without refreshing the page.
Open any .log, .jsonl, or .ndjson file from the sidebar to get a dedicated log viewer:
- Live Tail — toggle to continuously poll the file (every 1.5s) and stream new lines. Auto-scroll follows the tail only while you are at the bottom; if you scroll up to inspect older lines, it stays put.
- Clear — truncate the log file to zero length to easily see only the latest output.
- JSON table — when lines are JSON objects (one per line), render them as a table with columns inferred from the keys. Common fields (
time,level,msg, …) are ordered first andlevel/severityvalues get colored badges. Non-JSON lines fall back to raw rows.- Resizable columns — drag a column's right edge to resize. Widths are remembered per file.
- Hide columns — use the ⚙ Columns menu to toggle column visibility (or Reset).
- Per-column filters — each column has its own filter box; combine them (AND) with the global filter. Matches are highlighted.
- Filter all — top-level case-insensitive substring filter across the whole line (works in raw and table modes).
- 🔖 Views — Notion-like saved views bundling the filter + column configuration (visibility, widths, order, per-column filters). Saved views are stored in a
.mdviewerfile in the log's folder and are available to that folder and all subfolders. A view saved deeper in the tree overrides a same-named ancestor.
Column configuration is also auto-saved to localStorage per file, so reopening a log restores your last layout. Only the most recent ~2 MB of a large log is loaded initially; live tailing then streams new bytes incrementally and detects truncation/rotation.
GET /api/log?path=<rel>&offset=<n>returns new bytes sinceoffset(omitoffsetfor the initial tail). Response:{ content, offset, size, truncated }.POST /api/log/clear?path=<rel>truncates the log file.GET /api/log/views?path=<rel>lists saved views applicable to the log (own folder + ancestors up to root).POST /api/log/views/save?path=<rel>body{ name, config }— upserts a view in the log folder's.mdviewer.POST /api/log/views/delete?path=<rel>body{ name }— removes a view (searched deepest-first).
-root(default.): Root directory scanned recursively for Markdown files.-port(default8080): HTTP port to listen on.-podcast-watch(optional): Comma-separated list of directories and/or glob patterns to watch for auto podcast generation.-version: Print the version and exit.-update: Download the latest release binary for your platform from GitHub and replace the running executable in place, then exit.
mdviewer -version
# mdviewer v1.15.0
# Self-update to the latest GitHub release
mdviewer -updateAutomatically generates podcasts for new or updated markdown files in watched locations.
# Watch specific directories
mdviewer -root ./docs -podcast-watch "guides,blog/posts"
# Watch filename patterns (matches across ALL directories)
mdviewer -root ./workspace -podcast-watch "*-review.md,*-concepts.md"
# Mix directories and patterns
mdviewer -root ./workspace -podcast-watch "journal,01-projects/pi-router,*code-review*.md,*-concepts.md"How it works:
- Scans every 30 seconds for
.mdfiles in watched directories - Glob patterns (
*,?) match against filenames in any directory under root - Auto-triggers podcast generation when:
- A new
.mdfile appears without a.podcast.mp3sibling - An existing
.mdfile is newer than its.podcast.mp3(re-generates)
- A new
- One podcast generates at a time (queued) to avoid CPU overload
- State tracked in
~/.mdviewer/podcast-watch-state.json
Pattern rules:
- Entry containing
*or?→ treated as a glob pattern (matched against filename only) - Entry without wildcards → treated as a directory path (relative to
-root)
You can open a specific file on startup using the file query parameter:
http://localhost:8080/?file=docs/readme.md
For a shared full-screen view without the sidebar, add fullscreen=1 (or sidebar=hidden):
http://localhost:8080/?file=docs/readme.md&fullscreen=1
GET /api/healthreturns{"status":"ok"}.
- Go 1.22+
mkdir -p bin
go build -o bin/mdviewer .# Linux (amd64)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mdviewer-linux-amd64 .
# Linux (arm64)
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mdviewer-linux-arm64 .
# macOS Intel
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mdviewer-macos-amd64 .
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mdviewer-macos-arm64 .
# Windows (amd64)
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mdviewer-windows-amd64.exe .
# Windows (arm64)
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mdviewer-windows-arm64.exe .Pre-built binaries for Linux, macOS, and Windows are available on the Releases page.
- Download the binary for your platform from the latest release.
- Make it executable (Linux/macOS):
chmod +x mdviewer-* - Run it:
./mdviewer-linux-amd64 -root ~/notes -port 8080
macOS may block unsigned binaries. To allow it:
xattr -d com.apple.quarantine mdviewer-macos-arm64Or go to System Settings → Privacy & Security and click Open Anyway.
Releases are automated via GitHub Actions. To publish a new version:
git tag v1.1.0
git push origin v1.1.0This triggers the workflow in .github/workflows/release.yml, which builds binaries for all platforms and creates a GitHub Release with checksums.
mdviewer can generate a two-person podcast conversation from any markdown document. Click the 🎙️ button in the toolbar to generate.
How it works:
- An LLM reads the markdown and writes a natural two-person dialogue script
- Kokoro TTS (82M ONNX model) synthesizes speech with two distinct voices
- The result is an MP3 with natural pauses between speakers
The podcast is a real conversation — not a read-aloud. The host (Sarah) explains concepts using analogies and the guest (Michael) asks genuine questions and pushes back.
pip install kokoro-onnx pydub soundfile numpy# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# Windows (via chocolatey)
choco install ffmpegmkdir -p ~/.local/share/kokoro
curl -L "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx" \
-o ~/.local/share/kokoro/kokoro-v1.0.onnx
curl -L "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin" \
-o ~/.local/share/kokoro/voices-v1.0.binThe script must be in one of these locations:
- Same directory as the
mdviewerbinary /usr/local/share/mdviewer/podcast_gen.py~/src/markdown-go/podcast_gen.py
Set one of these (auto-detected in order):
| Provider | Environment Variables |
|---|---|
| Any OpenAI-compatible API | PODCAST_API_URL + PODCAST_API_TOKEN |
| OpenAI | OPENAI_API_KEY |
| Anthropic | ANTHROPIC_API_KEY |
Example with OpenAI:
export OPENAI_API_KEY="sk-..."
mdviewer -root ~/docs -port 8080Example with a local LLM (Ollama, LM Studio, etc.):
export PODCAST_API_URL="http://localhost:11434/v1/chat/completions"
mdviewer -root ~/docs -port 8080You can also use podcast_gen.py directly:
# Generate podcast with OpenAI
OPENAI_API_KEY="sk-..." python3 podcast_gen.py document.md
# Use a specific API endpoint
python3 podcast_gen.py document.md --api-url http://localhost:11434/v1/chat/completions
# Generate script only (no TTS)
python3 podcast_gen.py document.md --script-only
# Re-synthesize from existing script
python3 podcast_gen.py document.md --from-script document.podcast-script.txt
# Specify model
python3 podcast_gen.py document.md --model claude-opus-4.6- Script generation: 10-40s depending on LLM provider and document length
- TTS synthesis: ~1.6x realtime on CPU (a 3-min podcast takes ~5 min to generate)
- Output: MP3 at 128kbps, typically 2-6 min for a standard document
- No GPU required — runs entirely on CPU
Override default model locations with environment variables:
export KOKORO_MODEL="/path/to/kokoro-v1.0.onnx"
export KOKORO_VOICES="/path/to/voices-v1.0.bin"