I write a lot of Markdown — for this blog, for READMEs, for one-off docs that need to be emailed as a single self-contained HTML file. Pandoc is overkill. gh markdown-preview requires a network round-trip. Most lightweight converters either don’t do GitHub-flavoured Markdown properly or render output that looks like 2003. So I wrote mdtohtml.
What it is
mdtohtml is a Go CLI that converts Markdown to a standalone HTML file with GitHub-style CSS baked in. Under the hood it uses Goldmark — the same parser Hugo uses — with the GFM extensions enabled, plus footnotes, definition lists, smart typography and auto heading IDs.
The features I actually rely on day to day:
- Single-file conversion
- Batch mode with glob patterns and recursion
- A
validatecommand that checks Markdown syntax without writing output - GitHub-flavoured CSS so the result looks like a README, not a Word doc
Install
brew tap sgaunet/homebrew-tools
brew install sgaunet/tools/mdtohtml
Or grab a release from the releases page and drop it in your PATH. There’s also a Docker image for COPY --from= use.
Single file
The default verb is convert, but you can omit it:
mdtohtml README.md README.html
That’s the 80% case. The output is a complete HTML document — <head>, embedded CSS, the lot — so you can email it, drop it on a static server, or open it directly.
Batch mode
This is the one that earns its keep. I keep a docs/ folder full of Markdown and want to render the whole thing in one shot:
# All .md files in ./docs to ./html
mdtohtml batch ./docs --out-dir ./html
# Recursively, preserving structure
mdtohtml batch ./docs --recursive --out-dir ./output
# Custom glob — useful if you use .markdown
mdtohtml batch ./docs --pattern "*.markdown" --out-dir ./public
The batch command takes the same typography flags as convert, so if you don’t want smart quotes you can turn them off everywhere with --smartypants=false.
Validation
A small but useful subcommand: validate parses a Markdown file and exits non-zero if it fails. I run it in a pre-commit hook:
mdtohtml validate document.md
No output means valid. Combine with find and you get a cheap CI check that catches malformed files before they ship.
Typography flags
Goldmark’s typographer is enabled by default, so the CLI inherits it:
| Flag | Default | Effect |
|---|---|---|
--smartypants | true | "x" becomes curly quotes, ... becomes ellipsis |
--latexdashes | true | --- becomes em-dash, -- becomes en-dash |
--fractions | true | 1/2 becomes the proper Unicode fraction |
--safe-mode | false | Strip raw HTML to prevent XSS |
--safe-mode is the one to flip on if the Markdown source isn’t trusted.
Library usage
If you need to render Markdown from inside Go, the same converter is exposed as a library:
import (
"fmt"
"log"
"github.com/sgaunet/mdtohtml/pkg/converter"
)
func main() {
conv := converter.NewCompleteConverter(converter.DefaultOptions())
html, err := conv.Convert([]byte("# Hello\n\nWorld"))
if err != nil {
log.Fatal(err)
}
fmt.Println(string(html))
}
There’s also a NewCompleteConverterWithComponents constructor that lets you swap in your own TitleExtractor or HTMLTemplate implementation if the GitHub skin isn’t what you want.
Docker
The Docker image is there mostly so you can copy the binary into your own image:
FROM sgaunet/mdtohtml:latest AS mdtohtml
FROM alpine:latest
COPY --from=mdtohtml /usr/bin/mdtohtml /usr/bin/mdtohtml
Handy in a docs pipeline where you don’t want a full Go toolchain in the runtime image.
Where to find it
- Source: github.com/sgaunet/mdtohtml
- License: MIT
- Releases: Linux, macOS and Windows binaries
Open source, MIT-licensed. If you’ve ever wanted pandoc but smaller, with a sensible default skin and a one-line invocation, give it a try.