A local web server for browsing and reading markdown files with a clean, book-inspired interface.
Point it at any directory and get a navigable website with rendered markdown, syntax-highlighted code, YAML frontmatter display, wiki-link resolution, and auto-generated tables of contents.
Rendering docs/examples/overview.md
This was written primarily by Claude Code.
gem install markdownr
git clone https://github.com/brianmd/markdown-server.git
cd markdown-server
bundle install
ruby bin/markdownr [directory]
markdownr [options] [directory]
Serves the current directory if none is specified.
| Flag | Description | Default |
|---|---|---|
-p, --port PORT |
Port to listen on | 4567 |
-b, --bind ADDRESS |
Address to bind to | 127.0.0.1 |
-t, --title TITLE |
Custom page title | Directory name, titleized |
--allow-robots |
Allow search engine crawling | Disallowed |
--no-link-tooltips |
Disable preview tooltips on local markdown links | Enabled |
--standard-newlines |
Treat single newlines as spaces (standard markdown); default is Obsidian-style where single newlines become line breaks | Hard-wrap on |
-v, --version |
Show version and exit |
# Serve the current directory
markdownr
# Serve a specific directory on port 3000
markdownr -p 3000 ~/notes
# Serve with a custom title
markdownr -t "Field Notes" ~/research- GitHub Flavored Markdown -- tables, task lists, strikethrough, autolinks, and more (via Kramdown GFM)
- Syntax highlighting for fenced code blocks and standalone source files (via Rouge)
- YAML frontmatter parsed and displayed in a collapsible metadata table
- Wiki links --
[[page-name]]resolves to matching.mdfiles anywhere in the directory tree
- Auto-generated sticky sidebar for documents with multiple headings (on wide screens)
- Scroll spy highlights the current heading as you read
- Swipe-to-reveal TOC drawer on touch devices -- swipe left to open, swipe right to dismiss
- Floating TOC button on narrow screens for mouse users -- click to open the sliding drawer
- Tapping a heading in the drawer navigates there and closes the panel
- Full-text search across file contents within any directory subtree
- Searching from a markdown file shows only matches within that file
- Multi-word queries require all words to match (AND logic, any order)
- Each search term can be a regex (e.g.,
\d{4}ore.*him) - Results show matching lines with context, highlighted matches, and line numbers
- Clickable results jump directly to the matching line in the document
- Long lines are truncated with the match kept visible
- Search box available on every page (directories search within; files search their parent directory)
- Directory browsing with file sizes, modification dates, and sortable columns (name, modified, created)
- Sort persistence -- your chosen sort order is remembered across directories via localStorage
- Breadcrumb navigation on every page, auto-hides on scroll and reappears on scroll-up or tap
- Scroll position memory -- reopening a document returns you to the last heading you were reading
- JSON files rendered as syntax-highlighted YAML for readability
- PDF served in an inline viewer
- EPUB served as a download
- Source files (
.py,.rb,.js,.sh,.yaml,.html, etc.) displayed with syntax highlighting - Other text files shown as plain text; binary files served as downloads
- Hover preview -- hovering a local markdown link shows a popup with the linked document's content
- Click popup -- clicking a local markdown link shows the same popup; click again or follow to navigate
- Popup navigation -- links inside a popup load that document into the same popup, with a back button (←) to return to the previous document; browse several documents without leaving the page
- Popups auto-close on mouse leave; disabled with
--no-link-tooltips
- Wide-mode expand -- each table has a floating expand button (⤢) that widens the page to full viewport width, left-aligning the content for maximum reading area
- Tables scroll horizontally when they overflow on small screens
- Clean, book-inspired interface that adapts from desktop to mobile
- TOC transitions from a fixed sidebar to a swipe drawer on narrow screens
- Metadata tables reflow to stacked layout on mobile
| Extension | Rendering |
|---|---|
.md |
Rendered markdown with TOC |
.json |
Syntax-highlighted YAML |
.pdf |
Inline PDF viewer |
.epub |
Download |
.py, .rb, .sh, .js, .yaml, .html |
Syntax-highlighted source |
| Other text | Plain text display |
| Binary | Served as download |
flowchart TD
req[HTTP Request] --> route{Route}
route -->|GET /| redir[redirect → /browse/]
route -->|GET /robots.txt| robots[robots.txt response]
route -->|GET /download/*| dl[send_file as attachment]
route -->|GET /fetch| fetch[fetch external URL → JSON]
route -->|GET /preview/*| prev[render_markdown → JSON]
route -->|GET /search/*| srch[compile_regexes → search.erb]
route -->|GET /browse/*| sp{safe_path\ncheck}
sp -->|traversal / excluded| err[403 / 404]
sp -->|directory| rd[render_directory\n→ directory.erb]
sp -->|file| ext{Extension?}
ext -->|.md| md[parse_frontmatter\nrender_markdown\nextract_toc\n→ markdown.erb]
ext -->|.json| json[JSON → YAML\nsyntax_highlight\n→ raw.erb]
ext -->|.pdf| pdf[send inline]
ext -->|.epub| epub[redirect /download/]
ext -->|source files| src[syntax_highlight\n→ raw.erb]
ext -->|binary| bin[send as download]
The steps inside render_markdown must run in this order:
flowchart TD
A[Raw markdown text] --> B
B["1 · Resolve wiki links\n\[\[page\]\] and \[\[page|label\]\] → inline HTML\nMust run before Kramdown so the pipe character\nis not consumed as a GFM table delimiter"]
B --> C
C["2 · Kramdown GFM\nConverts markdown → HTML\nGenerates heading IDs and numbers all\nfootnote references sequentially (1, 2, 3…)"]
C --> D
D["3 · Restore footnote labels\nKramdown replaces \[^name\] with a number;\ntwo post-processing regexes restore the\noriginal label in both the inline superscript\nand the footnote list at the bottom"]
D --> E[Final HTML]
# Run unit and integration tests (101 tests, no browser required)
bundle exec rspec
# Run all tests including browser tests (127 tests, requires Chrome)
BROWSER_TESTS=1 bundle exec rspec| File | Tests | Coverage |
|---|---|---|
spec/helpers_spec.rb |
38 | parse_frontmatter, format_size/date, breadcrumbs, compile_regexes, extract_toc, render_markdown (wiki links, footnote label restoration, tables) |
spec/routes_spec.rb |
37 | All HTTP routes — redirects, directory/file rendering, frontmatter, TOC, search, downloads, 404/403, path traversal |
spec/layout_spec.rb |
26 | HTML/CSS/JS structure for the table wide-mode feature: right-sidebar div, expand button CSS, body.wide-mode rules, :has(:empty) logic, all JS identifiers |
spec/browser_spec.rb |
26 | Real Chrome: table DOM wrapping, expand button visibility, wide-mode toggle, two-table interactions, right-sidebar :empty CSS |
Browser tests use headless Chrome via Capybara and selenium-webdriver. On macOS, the spec automatically locates the version-matched chromedriver from the selenium cache, clears any Gatekeeper quarantine, and ad-hoc signs it before running.
- Ruby >= 3.0
MIT
