html-to-markdown

A robust Go library and CLI that converts HTML (even entire websites) into clean, readable Markdown, extensible through a priority-based plugin system.

Library
Go
vv1.6.0
3,809stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
55/100Fair
Development Activity32
Maintenance32
Community56
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
78/100Good
Architecture90
Code Quality82
Innovation84
Learning Curve55

html-to-markdown is a Go module for turning arbitrary HTML into well-formed Markdown, built around a Converter that runs registered handlers across pre-render, render, and post-render phases. Rather than hard-coding a fixed set of tag conversions, the library exposes a Register API so callers can register renderers per tag, mark tags as block/inline/removed, and reorder behavior with priorities — the shipped base and commonmark plugins are themselves built on this same public API, so anything the maintainers can do, consumers can do too.

Beyond the core conversion, it ships an escaping engine that only backslash-escapes Markdown-significant characters when they would actually be misinterpreted, a WithDomain option for rewriting relative links/images to absolute URLs, and optional plugins for GitHub-flavored tables and strikethrough. A companion CLI (html2markdown), distributed via Homebrew, Debian packages, and GoReleaser binaries, wraps the same library for terminal and pipe-based usage — reading files, glob patterns, or piped HTML from curl/echo and writing Markdown straight to stdout or disk.

What You Get

  • A Converter type with ConvertString/ConvertReader/ConvertNode entry points and a converter.NewConverter(WithPlugins(...)) builder for full control over which plugins are active
  • A Register API (Renderer, RendererFor, TagType, PreRenderer, PostRenderer, TextTransformer, UnEscaper) for writing custom conversion rules without forking the library
  • Built-in base and commonmark plugins implementing the CommonMark spec, plus optional table (GitHub-Flavored Markdown tables with rowspan/colspan) and strikethrough plugins
  • A smart escaping mode that only backslash-escapes characters when they would otherwise be parsed as Markdown syntax, documented in ESCAPING.md
  • WithDomain support for rewriting relative links and image sources into absolute URLs during conversion
  • A standalone html2markdown CLI (Homebrew tap, Debian package, prebuilt binaries, or go install) supporting file input/output, glob patterns, selector-based include/exclude, and plugin flags

Common Use Cases

  • Converting scraped web pages or entire crawled sites into Markdown for static-site generators, documentation pipelines, or LLM/RAG ingestion
  • Building a ‘export as Markdown’ or ‘copy as Markdown’ feature for a CMS, wiki, or note-taking app that stores content as HTML internally
  • Sanitizing/normalizing rich-text editor output (contenteditable HTML) into portable Markdown before storage
  • Piping HTML through the html2markdown CLI in shell scripts or CI jobs to batch-convert files or directories
  • Writing custom renderers for non-standard or Web Component tags via the Register API when the default CommonMark handling isn’t sufficient

Under The Hood

Architecture The library centers on a Converter struct (converter/converter.go) guarded by a sync.RWMutex, holding prioritized slices of pre-render, render, post-render, text-transform, and un-escape handlers plus a per-tag type map; conversion itself (converter/convert.go’s ConvertNode/ConvertReader/ConvertString) validates that required plugins (base, commonmark) are registered, builds a request-scoped Context carrying domain/URL-assembly state, then runs the three handler phases in priority order before returning the rendered bytes. Extension happens entirely through the Register type (converter/register.go) — RendererFor is a thin wrapper that combines TagType registration with a tag-filtered Renderer, and even the shipped base/commonmark/table/strikethrough plugins under plugin/ are implemented against this same public surface, so there’s no privileged internal API a third-party plugin author lacks access to. The table plugin’s own internal structure (plugin/table/1_select.go, 2_collect.go, 3_render.go) mirrors the library’s own multi-phase philosophy at a smaller scale. A separate cli/html2markdown module wraps the library for terminal use, and internal/ holds non-exported helpers (domutils, escape, textutils) that both the core and plugins depend on, keeping the public API surface deliberately small.

Tech Stack Written in Go 1.25 (go.mod declares module .../v2, a major-version-in-path signal of a deliberate breaking-change release from v1), with minimal runtime dependencies: golang.org/x/net/html for DOM parsing, github.com/JohannesKaufmann/dom (a companion package by the same author) for node-type helpers, andybalholm/cascadia for CSS selector matching (used by the CLI’s --include-selector/--exclude-selector), agnivade/levenshtein and bmatcuk/doublestar for supporting utilities, and yuin/goldmark plus sebdah/goldie pulled in as test-only golden-file tooling. The CLI is built and distributed via GoReleaser (.goreleaser.yaml), producing Homebrew, Debian/Cloudsmith, and prebuilt binary releases with version/commit/date baked in via ldflags or runtime/debug.ReadBuildInfo fallback.

Code Quality Testing is unusually rigorous for a library this size: 43 of 129 Go files are test files, and the project uses a custom “golden file” harness (internal/tester/goldenfiles.go, built on sebdah/goldie) where HTML fixtures in testdata/*.in.html are converted and diffed against checked-in *.out.md expected output, regenerable via go test -update — a pattern that scales well to catching conversion regressions across dozens of edge cases. CI (.github/workflows/go.yml) runs the full suite with -race on the latest stable Go, plus a second matrix across Go 1.25/1.26 on Ubuntu, macOS, and Windows. Errors are handled via typed sentinel values (errNoRenderHandlers, errBasePluginMissing) with wrapped context (fmt.Errorf("...: %w", err)) rather than panics, and shared mutable state is consistently protected by the Converter’s mutex with dedicated getter methods that copy-and-sort before returning handler slices. No golangci-lint config or CONTRIBUTING.md is present, but the CI/test discipline substitutes for a good deal of that.

API Design Getting started requires only three lines — import the package and call htmltomarkdown.ConvertString(html) — while converter.NewConverter(WithPlugins(...)) exposes the full extension surface for callers who need it. Naming is consistent (With* functional options, Register.* for extension points, TagType* constants), and the README pairs every capability with a runnable example under examples/ (basics, options, register) rather than prose alone. The v2 module path change was used specifically to signal the breaking API redesign while leaving a v1 branch intact for existing consumers, and the CLI mirrors the library’s own flag naming (--domain, --exclude-selector, --plugin-table) so switching between the two doesn’t require relearning conventions.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search