From eb83bbc2a210ac77138e503d9f3e7e83d805e103 Mon Sep 17 00:00:00 2001 From: clavin Date: Tue, 7 Oct 2025 00:11:05 -0600 Subject: [PATCH 1/2] feat: add markdown output plugin to copy markdown files alongside HTML --- docusaurus.config.ts | 1 + src/plugins/markdown-output/index.ts | 95 ++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/plugins/markdown-output/index.ts diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 23b3e6c92..5b3222647 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -242,6 +242,7 @@ const config: Config = { path.resolve(__dirname, './src/plugins/releases/index.ts'), path.resolve(__dirname, './src/plugins/fiddle/index.ts'), path.resolve(__dirname, './src/plugins/governance/index.ts'), + path.resolve(__dirname, './src/plugins/markdown-output/index.ts'), ], presets: [ [ diff --git a/src/plugins/markdown-output/index.ts b/src/plugins/markdown-output/index.ts new file mode 100644 index 000000000..318bca0e4 --- /dev/null +++ b/src/plugins/markdown-output/index.ts @@ -0,0 +1,95 @@ +/** + * Plugin to output index.md files alongside index.html in the build directory. + * This copies the original markdown source files to their corresponding locations + * in the build output. + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { Plugin } from '@docusaurus/types'; +import logger from '@docusaurus/logger'; + +/** Route prefixes to copy markdown files within. */ +const PREFIXES = ['docs/', 'blog/']; + +type Context = Parameters[0]; + +async function fileExists(filePath: string): Promise { + return fs.access(filePath).then( + () => true, + () => false, + ); +} + +async function findMarkdownSource( + routePath: string, + { siteDir }: Context, +): Promise { + for (const prefix of PREFIXES) { + const routePrefix = `/${prefix}`; + + if (!routePath.startsWith(routePrefix)) continue; + + // Since routePrefix is absolute and routePath starts with routePrefix, it too must be absolute + // meaning we can safely use path.relative to get the relative path + const relativePath = path.relative(routePrefix, routePath); + const sourceDir = path.join(siteDir, prefix); + + // Try direct file path first (e.g., /docs/tutorial/intro.md) + const directPath = path.join(sourceDir, `${relativePath}.md`); + if (await fileExists(directPath)) return directPath; + + // Try index.md in directory (e.g., /docs/tutorial/intro/index.md) + const indexPath = path.join(sourceDir, relativePath, 'index.md'); + if (await fileExists(indexPath)) return indexPath; + } + + return null; +} + +async function copyMarkdownForRoute( + routePath: string, + context: Context, +): Promise { + const { outDir } = context; + + // Find corresponding markdown source + const mdSourcePath = await findMarkdownSource(routePath, context); + if (!mdSourcePath) return false; + + // Check if HTML output exists + const htmlPath = path.join(outDir, routePath, 'index.html'); + if (!(await fileExists(htmlPath))) return false; + + // Copy markdown to output directory + const outputMdPath = path.join(outDir, routePath, 'index.md'); + await fs.copyFile(mdSourcePath, outputMdPath); + + return true; +} + +export default async function markdownOutputPlugin(): Promise { + return { + name: 'markdown-output-plugin', + async postBuild(context) { + logger.info('Copying markdown files to build directory...'); + + // Filter to only routes in target prefixes + const routes = context.routesPaths.filter((routePath) => + PREFIXES.some((prefix) => routePath.startsWith(`/${prefix}`)), + ); + + // Process all routes in parallel + const results = await Promise.allSettled( + routes.map((routePath) => copyMarkdownForRoute(routePath, context)), + ); + + // Count successful copies + const copiedCount = results.filter( + (result) => result.status === 'fulfilled' && result.value, + ).length; + + logger.success(`Copied ${copiedCount} markdown files to build directory`); + }, + }; +} From bc690b66bb1fb7392285767783c6fc78252b8aec Mon Sep 17 00:00:00 2001 From: clavin Date: Tue, 7 Oct 2025 20:26:15 -0600 Subject: [PATCH 2/2] Remove async, simplify --- src/plugins/markdown-output/index.ts | 48 +++++++++++----------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/plugins/markdown-output/index.ts b/src/plugins/markdown-output/index.ts index 318bca0e4..04d4144b0 100644 --- a/src/plugins/markdown-output/index.ts +++ b/src/plugins/markdown-output/index.ts @@ -4,7 +4,7 @@ * in the build output. */ -import fs from 'node:fs/promises'; +import fs from 'node:fs'; import path from 'node:path'; import { Plugin } from '@docusaurus/types'; import logger from '@docusaurus/logger'; @@ -14,17 +14,10 @@ const PREFIXES = ['docs/', 'blog/']; type Context = Parameters[0]; -async function fileExists(filePath: string): Promise { - return fs.access(filePath).then( - () => true, - () => false, - ); -} - -async function findMarkdownSource( +function findMarkdownSource( routePath: string, { siteDir }: Context, -): Promise { +): string | null { for (const prefix of PREFIXES) { const routePrefix = `/${prefix}`; @@ -37,41 +30,38 @@ async function findMarkdownSource( // Try direct file path first (e.g., /docs/tutorial/intro.md) const directPath = path.join(sourceDir, `${relativePath}.md`); - if (await fileExists(directPath)) return directPath; + if (fs.existsSync(directPath)) return directPath; // Try index.md in directory (e.g., /docs/tutorial/intro/index.md) const indexPath = path.join(sourceDir, relativePath, 'index.md'); - if (await fileExists(indexPath)) return indexPath; + if (fs.existsSync(indexPath)) return indexPath; } return null; } -async function copyMarkdownForRoute( - routePath: string, - context: Context, -): Promise { +function copyMarkdownForRoute(routePath: string, context: Context): boolean { const { outDir } = context; // Find corresponding markdown source - const mdSourcePath = await findMarkdownSource(routePath, context); + const mdSourcePath = findMarkdownSource(routePath, context); if (!mdSourcePath) return false; // Check if HTML output exists const htmlPath = path.join(outDir, routePath, 'index.html'); - if (!(await fileExists(htmlPath))) return false; + if (!fs.existsSync(htmlPath)) return false; // Copy markdown to output directory const outputMdPath = path.join(outDir, routePath, 'index.md'); - await fs.copyFile(mdSourcePath, outputMdPath); + fs.copyFileSync(mdSourcePath, outputMdPath); return true; } -export default async function markdownOutputPlugin(): Promise { +export default function markdownOutputPlugin(): Plugin { return { name: 'markdown-output-plugin', - async postBuild(context) { + postBuild(context) { logger.info('Copying markdown files to build directory...'); // Filter to only routes in target prefixes @@ -79,15 +69,13 @@ export default async function markdownOutputPlugin(): Promise { PREFIXES.some((prefix) => routePath.startsWith(`/${prefix}`)), ); - // Process all routes in parallel - const results = await Promise.allSettled( - routes.map((routePath) => copyMarkdownForRoute(routePath, context)), - ); - - // Count successful copies - const copiedCount = results.filter( - (result) => result.status === 'fulfilled' && result.value, - ).length; + // Process all routes + let copiedCount = 0; + for (const routePath of routes) { + if (copyMarkdownForRoute(routePath, context)) { + copiedCount += 1; + } + } logger.success(`Copied ${copiedCount} markdown files to build directory`); },