From e50c58924e5ee8dcb37279b46f21b33becf14288 Mon Sep 17 00:00:00 2001 From: Moiz Haider Date: Thu, 12 Mar 2026 14:25:56 +0500 Subject: [PATCH 1/2] feat: serve raw markdown via .md endpoint --- app/docs-raw/[...slug]/route.dev.ts | 21 +++++++++++++ next.config.mjs | 15 ++++++++- package.json | 2 +- scripts/copy-docs-md.ts | 47 +++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 app/docs-raw/[...slug]/route.dev.ts create mode 100644 scripts/copy-docs-md.ts diff --git a/app/docs-raw/[...slug]/route.dev.ts b/app/docs-raw/[...slug]/route.dev.ts new file mode 100644 index 0000000..8153e92 --- /dev/null +++ b/app/docs-raw/[...slug]/route.dev.ts @@ -0,0 +1,21 @@ +import { getDoc } from "@/lib/docs" +import { parseDocSlug } from "@/lib/docs-config" + +// In production, .md files are served as static files written by scripts/copy-docs-md.ts. +// This route only runs in dev (output: "export" is not set in dev). +export const dynamic = "force-dynamic" + +export async function GET( + _request: Request, + { params }: { params: Promise<{ slug: string[] }> } +) { + const { slug } = await params + const { version, docSlug } = parseDocSlug(slug) + const doc = getDoc(version, docSlug) + + if (!doc) return new Response("Not found", { status: 404 }) + + return new Response(doc.content, { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }) +} diff --git a/next.config.mjs b/next.config.mjs index 9d27a93..42cf490 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,9 +1,22 @@ /** @type {import('next').NextConfig} */ +const isDev = process.env.NODE_ENV === "development" + const nextConfig = { - output: "export", + output: isDev ? undefined : "export", + pageExtensions: isDev + ? ["tsx", "ts", "jsx", "js", "dev.ts", "dev.tsx"] + : ["tsx", "ts", "jsx", "js"], images: { unoptimized: true, }, + async rewrites() { + return [ + { + source: "/docs/:path*.md", + destination: "/docs-raw/:path*", + }, + ] + }, } export default nextConfig diff --git a/package.json b/package.json index 4cbd89c..ffc5179 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev --turbopack", - "build": "next build && bun run scripts/generate-rss.ts", + "build": "next build && bun run scripts/generate-rss.ts && bun run scripts/copy-docs-md.ts", "start": "next start", "lint": "eslint", "format": "prettier --write \"**/*.{ts,tsx}\"", diff --git a/scripts/copy-docs-md.ts b/scripts/copy-docs-md.ts new file mode 100644 index 0000000..7bc233b --- /dev/null +++ b/scripts/copy-docs-md.ts @@ -0,0 +1,47 @@ +import fs from "fs" +import path from "path" +import matter from "gray-matter" +import { VERSIONS, DEFAULT_VERSION, type Version } from "../lib/docs-config" + +const CONTENT_DIR = path.join(process.cwd(), "content/docs") +const OUT_DIR = path.join(process.cwd(), "out/docs") + +function copyMarkdownFiles() { + let count = 0 + + for (const version of VERSIONS) { + const versionDir = path.join(CONTENT_DIR, version) + if (!fs.existsSync(versionDir)) continue + + const targetDir = + version === DEFAULT_VERSION ? OUT_DIR : path.join(OUT_DIR, version) + + function walk(dir: string, relativePath: string = "") { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + walk(fullPath, path.join(relativePath, entry.name)) + } else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) { + const rawContent = fs.readFileSync(fullPath, "utf-8") + const { content } = matter(rawContent) + + const mdName = entry.name.replace(/\.(mdx|md)$/, ".md") + const outPath = path.join(targetDir, relativePath, mdName) + + fs.mkdirSync(path.dirname(outPath), { recursive: true }) + fs.writeFileSync(outPath, content.trim() + "\n") + count++ + } + } + } + + walk(versionDir) + } + + console.log(`Copied ${count} markdown files → out/docs/`) +} + +copyMarkdownFiles() From fba77b673e26cc1fadec41b1bc385cd79ccb8225 Mon Sep 17 00:00:00 2001 From: Moiz Haider Date: Thu, 12 Mar 2026 14:32:03 +0500 Subject: [PATCH 2/2] feat: add 'Copy as Markdown' button to TOC --- app/docs/[...slug]/page.tsx | 2 +- components/table-of-contents.tsx | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index 1d536c7..d1e89ad 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -104,7 +104,7 @@ export default async function DocPage({ params }: PageProps) { {/* Table of Contents - right sidebar */} {headings.length > 0 && ( )} diff --git a/components/table-of-contents.tsx b/components/table-of-contents.tsx index 4475bfa..555911d 100644 --- a/components/table-of-contents.tsx +++ b/components/table-of-contents.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react" import { cn } from "@/lib/utils" +import { CopyIcon, CheckIcon } from "lucide-react" interface TocEntry { depth: number @@ -9,8 +10,22 @@ interface TocEntry { id: string } -export function TableOfContents({ headings }: { headings: TocEntry[] }) { +export function TableOfContents({ + headings, + rawContent, +}: { + headings: TocEntry[] + rawContent?: string +}) { const [activeId, setActiveId] = useState("") + const [copied, setCopied] = useState(false) + + async function copyMarkdown() { + if (!rawContent) return + await navigator.clipboard.writeText(rawContent) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } useEffect(() => { if (headings.length === 0) return @@ -46,6 +61,19 @@ export function TableOfContents({ headings }: { headings: TocEntry[] }) { return (