Skip to content

Instantly share code, notes, and snippets.

@pbzona
Created December 16, 2025 19:39
Show Gist options
  • Select an option

  • Save pbzona/790588de9b981981f6269e62bcbe965d to your computer and use it in GitHub Desktop.

Select an option

Save pbzona/790588de9b981981f6269e62bcbe965d to your computer and use it in GitHub Desktop.
Quick zero dependency markdown server
#!/usr/bin/env bun
import { readdir, readFile } from "node:fs/promises";
import { join, relative } from "node:path";
// Simple markdown to HTML converter (basic support)
function markdownToHtml(markdown: string): string {
let html = markdown;
// Headers
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
// Italic
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
// Code blocks
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>");
// Inline code
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Line breaks and paragraphs
html = html
.split("\n\n")
.map((para) => {
para = para.trim();
if (
para.startsWith("<h") ||
para.startsWith("<pre>") ||
para.startsWith("<ul>") ||
para.startsWith("<ol>")
) {
return para;
}
return para ? `<p>${para.replace(/\n/g, "<br>")}</p>` : "";
})
.join("\n");
// Unordered lists
html = html.replace(/^- (.+)$/gim, "<li>$1</li>");
html = html.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>");
return html;
}
// Recursively find all .md and .mdx files
async function findMarkdownFiles(dir: string, baseDir: string): Promise<string[]> {
const files: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await findMarkdownFiles(fullPath, baseDir)));
} else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) {
files.push(relative(baseDir, fullPath));
}
}
return files;
}
const PORT = 3001;
const DOCS_DIR = import.meta.dir;
// Find all markdown files
const markdownFiles = await findMarkdownFiles(DOCS_DIR, DOCS_DIR);
// Filter out the serve-docs.ts file itself
const mdxFiles = markdownFiles.filter(f => !f.endsWith(".ts"));
Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url);
// Default to first markdown file if exists
const defaultFile = mdxFiles.length > 0 ? `/${mdxFiles[0]}` : "/README.md";
let path = url.pathname === "/" ? defaultFile : url.pathname;
// Remove leading slash
if (path.startsWith("/")) {
path = path.substring(1);
}
// If no extension, assume .md
if (!path.endsWith(".md") && !path.endsWith(".mdx")) {
path = `${path}.md`;
}
try {
const filePath = join(DOCS_DIR, path);
const content = await readFile(filePath, "utf-8");
const html = markdownToHtml(content);
// Create navigation
const nav = mdxFiles
.sort()
.map((file) => {
const displayName = file
.replace(/\.mdx$/, "")
.replace(/\//g, " / ")
.replace(/-/g, " ");
const isActive = file === path;
return `<div class="${isActive ? "active" : ""}"><a href="/${file}">${displayName}</a></div>`;
})
.join("\n");
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Docs - ${path}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #1a1a1a;
color: #e0e0e0;
line-height: 1.6;
display: flex;
min-height: 100vh;
}
nav {
width: 280px;
background: #242424;
padding: 20px;
overflow-y: auto;
border-right: 1px solid #333;
position: sticky;
top: 0;
height: 100vh;
}
nav h2 {
font-size: 18px;
margin-bottom: 16px;
color: #fff;
}
nav a {
color: #a0a0a0;
text-decoration: none;
display: block;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s;
}
nav a:hover {
background: #2a2a2a;
color: #fff;
}
nav .active a {
background: #3a3a3a;
color: #fff;
font-weight: 500;
}
main {
flex: 1;
padding: 40px 60px;
max-width: 900px;
}
h1 {
font-size: 2.5em;
margin-bottom: 0.5em;
color: #fff;
}
h2 {
font-size: 2em;
margin-top: 1.5em;
margin-bottom: 0.5em;
color: #fff;
border-bottom: 1px solid #333;
padding-bottom: 0.3em;
}
h3 {
font-size: 1.5em;
margin-top: 1.2em;
margin-bottom: 0.5em;
color: #fff;
}
p {
margin-bottom: 1em;
}
code {
background: #2a2a2a;
padding: 2px 6px;
border-radius: 3px;
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
font-size: 0.9em;
color: #4fc1ff;
}
pre {
background: #2a2a2a;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
margin: 1em 0;
border: 1px solid #333;
}
pre code {
background: none;
padding: 0;
color: #e0e0e0;
}
a {
color: #4fc1ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ul, ol {
margin-left: 2em;
margin-bottom: 1em;
}
li {
margin-bottom: 0.5em;
}
strong {
color: #fff;
font-weight: 600;
}
</style>
</head>
<body>
<nav>
<h2>Documentation</h2>
${nav}
</nav>
<main>
${html}
</main>
</body>
</html>
`;
return new Response(fullHtml, {
headers: { "Content-Type": "text/html" },
});
} catch {
return new Response(`<h1>404 - Not Found</h1><p>Could not find ${path}</p>`, {
status: 404,
headers: { "Content-Type": "text/html" },
});
}
},
});
console.log(`📚 Docs server running at http://localhost:${PORT}`);
console.log(`Found ${mdxFiles.length} markdown files`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment