Created
December 16, 2025 19:39
-
-
Save pbzona/790588de9b981981f6269e62bcbe965d to your computer and use it in GitHub Desktop.
Quick zero dependency markdown server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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