mo: a Markdown viewer that turns your .md files into a browsable documentation site
mo is a Markdown viewer that opens .md files in a browser.
At a glance
- What is it?
- mo is a Go tool that serves Markdown files to a browser with live reload, GitHub flavoured syntax, Mermaid diagrams, KaTeX math and a file tree sidebar, and it reuses one server on port 6275 so successive invocations accumulate into one session. The cost is that it is a local viewing tool with a fixed feature set, not a documentation generator you can theme into a site.
- Who is it for?
- mo fits a developer who reviews or writes Markdown and wants to read it the way a reader will see it, with syntax highlighting, tables, footnotes, Mermaid and math rendering, without leaving the terminal or writing a static site generator configuration.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One server on port 6275 that accumulates your documents
mo is a Markdown viewer that opens .md files in a browser, and the first thing to understand is that it behaves like a session server rather than a one shot converter.
By default mo runs a single server on port 6275. If a server is already running on the same port, subsequent mo invocations add files to the existing session instead of starting a new one.
$ mo README.md # Starts a mo server in the background
$ mo CHANGELOG.md # Adds the file to the running mo serverTo run a completely separate session you use a different port, for example mo draft.md -p 6276.
That design is the tool's central idea and it cuts both ways. On the good side, opening ten files is ten quick invocations against one server, and the sidebar accumulates them, so you end up with one browser tab showing your whole working set rather than ten tabs. On the bad side, the sessions are implicit. Nothing tells you that the server on 6275 already has three files in it, and a second document you open lands in the same sidebar. If you are working on two unrelated pieces of documentation at once, the second one appears in the first one's browser view until you use a different port.
The upstream command is spf13/cobra, and the browser is opened through github.com/pkg/browser, so the tool opens your default browser rather than trying to be a browser itself.
Groups, which are URL paths rather than separate servers
Files can be organized into named groups using the --target, or -t, flag, and each group gets its own URL path and its own sidebar.
$ mo spec.md --target design # Opens at http://localhost:6275/design
$ mo api.md --target design # Adds to the "design" group
$ mo notes.md --target notes # Opens at http://localhost:6275/notesNote that groups are not servers. Everything lands on the same port 6275, and the group is a path segment. That is a good design, because it means you can have as many logically separate views as you want without juggling ports, and a shared group is just a second invocation with the same --target.
The difference from the port mechanism is worth stating, because the two solve opposite problems. The port separates sessions, which you want when two documents should not be visible to each other. The group separates views inside one session, which you want when several documents belong to the same project but should be navigable independently.
The sidebar also has a display toggle per group, which the feature list describes as a file name versus heading title toggle, per group. So the same session can show one group by filename and another by its first heading, which matters when the files are named README.md and design.md and the headings are the meaningful titles.
The repository root carries a .tagpr file, which is the configuration for tagpr, the changelog and tag tool the project uses for its own releases. The CHANGELOG.md at the root and the prerelease_for_tagpr target in the Makefile, which regenerates the credits and stages the changelog, the credits and the module files, show the release process is also a repository of its own.
Watch mode, and the difference between a glob and a directory
The --watch, or -w, flag turns on watch mode. Directory and glob positional arguments are registered as watch patterns, matching files are opened, and new matching files are picked up automatically.
$ mo -w '**/*.md' # Watch and open all .md files recursively
$ mo -w 'docs/**/*.md' --target docs # Watch docs/ tree in "docs" group
$ mo -w '*.md' 'docs/**/*.md' # Multiple patterns (positional)
$ mo -w docs/ # Watch docs/*.mdCombine it with --recursive, or -R, to descend into subdirectories, and the short flags can be combined, so mo -wR docs/ is the same as mo -w -R docs/.
The distinction between watch mode and plain glob expansion is the subtle part. Without --watch, globs are expanded once and directory arguments open matching files without live watching for new additions.
$ mo docs/ # Open every .md directly in docs/
$ mo -R docs/ # Open every .md under docs/ (recursive)
$ mo 'docs/*.md' # Expand and open matching .md filesSo a directory argument without -R is non recursive, and a glob is expanded at the moment you run the command. With -w, both become live: new files appearing under a watched directory show up in the sidebar without another invocation. The glob expansion is handled by bmatcuk/doublestar, and file system events by github.com/fswatcher/fswatcher, so the watching is native rather than a polling loop.
There is an --unwatch flag that removes previously registered patterns, taking globs or directories as positional arguments. The documentation is careful about a distinction that would otherwise bite you: regular file paths are not accepted for unwatching, because removing an individual file from the sidebar is what --close is for. And files that were already added by a pattern remain in the sidebar after the pattern is removed.
Stdin, and the in-memory entry that is not live reloaded
There are two ways to get content into mo without giving it a path, and the behaviour differs from a file in a way the documentation spells out.
When no positional arguments are given and stdin is redirected rather than being a terminal, mo reads Markdown content from stdin.
$ cat notes.md | mo
$ some-command | mo --target output
$ mo < notes.mdThe content is loaded in memory with a generated name of the form stdin-<hash>.md, and piping the same content again reuses the existing entry, deduplicated by content hash. That is a small but thoughtful detail: piping the same file twice does not give you two copies in the sidebar.
The same in memory rule applies to the other non file path into the tool, drag and drop file addition from the OS file manager. The feature list states that the content is loaded in memory and that live reload is not supported for dropped files.
So live reload, which is the headline behaviour, applies to files opened via the command line. That is stated plainly in the feature list: live reload on save, for files opened via CLI.
The distinction is sensible but it is a real limitation, and it is the one place where the tool's promise does not hold for every input. If you are reviewing a document by dropping it into the window, you reload the page yourself. If you opened it by path, saving in your editor updates the browser. The in memory entries are the ones you cannot edit in place and expect to see the result.
The rendering surface, and where mo stops short of a static site
The rendering is what makes mo more than a plain text to HTML converter, and the list is worth reading in full.
The base is GitHub flavoured Markdown, with tables, task lists and footnotes. Syntax highlighting is done with Shiki. Mermaid diagrams are rendered. LaTeX math is rendered with KaTeX. GitHub Alerts, that is, admonitions, are supported. There is a fullscreen zoom modal for images and for Mermaid diagrams.
Then there is the interface layer, which is where mo becomes a viewer rather than a converter. There is a dark and a light theme. There is file grouping, a table of contents panel, and a flat view against a tree sidebar view with drag and drop reordering. The sidebar can show the file name or the heading title, toggled per group. There is full text search across file names and content. YAML frontmatter is displayed as a collapsible metadata block.
The raw view and the copy feature are small but telling. You can switch to a raw markdown view, and you can copy the content as Markdown, as plain text or as HTML, which means mo doubles as a converter for a single document without leaving the browser.
There is also MDX support, where the file renders as Markdown with import and export statements stripped and JSX tags escaped. That is a defensive approach rather than a full MDX implementation, and it is worth understanding as such, because it means MDX components do not execute.
The front end is a separate project. The Makefile shows the front end living in internal/frontend, built and tested with pnpm, with a generate target that runs go generate over internal/static to embed the built assets into the Go binary. There is a screenshot target, which is how the images in the README are produced.
What mo does not do is generate a site. There is no output directory, no theme you can restyle beyond the light and dark toggle, and no navigation file you author. It is a viewer for your own working set of documents, and the table of contents panel is the closest thing it has to a site navigation.
Installing it, and the toolchain a source build needs
Two install routes are documented.
$ brew install k1LoW/tap/moOr manually, by downloading the binary from the releases page.
The go.mod tells you what the tool depends on, and the list is short. cobra at 1.10.2 for the command line, pflag and mousetrap underneath it, golang.org/x/text at 0.42.0, muesli/termenv at 0.16.0 for terminal colour, k1LoW/donegroup for goroutine lifecycle, k1LoW/errors for error wrapping, doublestar for glob matching, fswatcher for file system events, and pkg/browser to open the browser. The module requires Go 1.26.8.
There is also a small set of indirect dependencies that hint at what the binary does at the edges. go-osc52 with a purego backend is how termenv emits colour sequences, and lucasb-eyer/go-colorful and rivo/uniseg are termenv's colour and width handling. mattn/go-isatty is the terminal detection that decides whether to colourise output at all.
The Makefile is where the development story is, and it is a two language project. The default target runs the tests, which means a pnpm install and a pnpm run test:coverage in internal/frontend, followed by go test across the module. The build target runs generate first and then compiles with a linker flag that stamps the git revision into the binary, with -trimpath. The dev target builds and runs the binary in the foreground on port 16275. The lint target runs the front end formatter and linter, then golangci-lint, then go vet with the gostyle vettool against a .gostyle.yml configuration.
So a source build is not only go build. The front end is a pnpm project, the assets are generated into internal/static, and the Go binary embeds them.
Editorial conclusion
mo fits a developer who reviews or writes Markdown and wants to read it the way a reader will see it, with syntax highlighting, tables, footnotes, Mermaid and math rendering, without leaving the terminal or writing a static site generator configuration. It does not fit a team that needs to publish a documentation site, because there is no output directory, no theme pipeline and no navigation generation beyond the table of contents panel, and it does not fit a workflow that needs the browser to stay open on a shared or remote machine. Check three things before you adopt it. Check how the server behaves, because a single server runs on port 6275 and later invocations add files to the running session rather than starting a second one, so two independent documents collide unless you use a different port. Check the live reload scope, because it applies to files opened from the command line and the documentation is explicit that dropped files are loaded in memory and are not live reloaded. And check the front end build, because the Go binary embeds a frontend that is generated from internal/frontend with pnpm, so a source build needs Node tooling as well as Go. The last push was on 2026-10-08, the newest release is v1.6.9 from 2026-09-28, and the license is MIT.
Frequently asked questions
How do I install and run mo?
Install with brew install k1LoW/tap/mo, or download the binary from the releases page. Then run mo README.md to open a single file, mo docs/ to open every .md in a directory, or mo README.md CHANGELOG.md docs/*.md for several at once. The browser opens on port 6275.
What happens if I run mo twice on the same port?
By default mo runs a single server on port 6275, and if a server is already running on the same port, later invocations add their files to the existing session instead of starting a new server. Use a different port, such as mo draft.md -p 6276, for a completely separate session.
How do groups work in mo?
Files are organized into named groups with the --target, or -t, flag, and each group gets its own URL path and sidebar on the same port. For example mo spec.md --target design opens at http://localhost:6275/design.
Does mo support live reload for every file it opens?
Live reload on save applies to files opened via the command line. Files dropped in from the OS file manager are loaded in memory and are not live reloaded, and the same is true of content piped in through stdin.
Which Markdown features does mo render?
GitHub flavoured Markdown including tables, task lists and footnotes, syntax highlighting with Shiki, Mermaid diagram rendering, LaTeX math with KaTeX, and GitHub Alerts as admonitions. MDX files render as Markdown with import and export stripped and JSX tags escaped.
What license is mo released under?
MIT, with a LICENSE file at the root of the repository. The newest listed release is v1.6.9 from 2026-09-28, and the last push was on 2026-10-08.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/k1low-mo)Was this useful?